package de.effigenix.application.masterdata;

import de.effigenix.application.masterdata.article.*;
import de.effigenix.application.masterdata.article.command.*;
import de.effigenix.application.masterdata.article.AssignSupplier;
import de.effigenix.application.masterdata.supplier.RemoveSupplier;
import de.effigenix.application.masterdata.article.command.AssignSupplierCommand;
import de.effigenix.application.masterdata.supplier.command.RemoveSupplierCommand;
import de.effigenix.domain.masterdata.*;
import de.effigenix.domain.masterdata.article.*;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryId;
import de.effigenix.domain.masterdata.supplier.SupplierId;
import de.effigenix.shared.common.Money;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.function.Supplier;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("Article Use Cases")
class ArticleUseCaseTest {

    @Mock private ArticleRepository articleRepository;
    @Mock private UnitOfWork unitOfWork;

    private final ActorId performedBy = ActorId.of("admin-user");
    private final String CATEGORY_ID = UUID.randomUUID().toString();
    private final OffsetDateTime NOW = OffsetDateTime.now(ZoneOffset.UTC);

    // ==================== Helpers ====================

    private Article createValidArticle() {
        var draft = new ArticleDraft("Testprodukt", "ART-001", CATEGORY_ID,
                Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
        return Article.create(draft).unsafeGetValue();
    }

    private Article reconstitutedArticle(String id) {
        return Article.reconstitute(
                ArticleId.of(id),
                new ArticleName("Testprodukt"),
                new ArticleNumber("ART-001"),
                ProductCategoryId.of(CATEGORY_ID),
                new ArrayList<>(List.of(
                        SalesUnit.reconstitute(
                                SalesUnitId.of("su-1"),
                                Unit.PIECE_FIXED,
                                PriceModel.FIXED,
                                Money.euro(new BigDecimal("9.99"))
                        )
                )),
                ArticleStatus.ACTIVE,
                new HashSet<>(),
                NOW,
                NOW
        );
    }

    private Article reconstitutedArticleWithTwoUnits(String id) {
        return Article.reconstitute(
                ArticleId.of(id),
                new ArticleName("Testprodukt"),
                new ArticleNumber("ART-001"),
                ProductCategoryId.of(CATEGORY_ID),
                new ArrayList<>(List.of(
                        SalesUnit.reconstitute(
                                SalesUnitId.of("su-1"),
                                Unit.PIECE_FIXED,
                                PriceModel.FIXED,
                                Money.euro(new BigDecimal("9.99"))
                        ),
                        SalesUnit.reconstitute(
                                SalesUnitId.of("su-2"),
                                Unit.KG,
                                PriceModel.WEIGHT_BASED,
                                Money.euro(new BigDecimal("19.99"))
                        )
                )),
                ArticleStatus.ACTIVE,
                new HashSet<>(),
                NOW,
                NOW
        );
    }

    // ==================== CreateArticle ====================

    @Nested
    @DisplayName("CreateArticle")
    class CreateArticleTest {

        private CreateArticle createArticle;

        @BeforeEach
        void setUp() {
            lenient().when(unitOfWork.executeAtomically(any()))
                    .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
            createArticle = new CreateArticle(articleRepository, unitOfWork);
        }

        @Test
        @DisplayName("should create article successfully")
        void shouldCreateArticle() {
            when(articleRepository.existsByArticleNumber(any()))
                    .thenReturn(Result.success(false));
            when(articleRepository.save(any()))
                    .thenReturn(Result.success(null));

            var cmd = new CreateArticleCommand("Testprodukt", "ART-001", CATEGORY_ID,
                    Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));

            var result = createArticle.execute(cmd, performedBy);

            assertThat(result.isSuccess()).isTrue();
            var article = result.unsafeGetValue();
            assertThat(article.name().value()).isEqualTo("Testprodukt");
            assertThat(article.articleNumber().value()).isEqualTo("ART-001");
            assertThat(article.status()).isEqualTo(ArticleStatus.ACTIVE);
            assertThat(article.salesUnits()).hasSize(1);
            verify(articleRepository).save(any());
        }

        @Test
        @DisplayName("should fail when article number already exists")
        void shouldFailWhenArticleNumberExists() {
            when(articleRepository.existsByArticleNumber(any()))
                    .thenReturn(Result.success(true));

            var cmd = new CreateArticleCommand("Testprodukt", "ART-001", CATEGORY_ID,
                    Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));

            var result = createArticle.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ArticleNumberAlreadyExists.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with ValidationFailure when name is blank")
        void shouldFailWhenNameIsBlank() {
            var cmd = new CreateArticleCommand("", "ART-001", CATEGORY_ID,
                    Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));

            var result = createArticle.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
            verifyNoInteractions(articleRepository);
        }

        @Test
        @DisplayName("should fail with ValidationFailure when article number is blank")
        void shouldFailWhenArticleNumberIsBlank() {
            var cmd = new CreateArticleCommand("Testprodukt", "", CATEGORY_ID,
                    Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));

            var result = createArticle.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
            verifyNoInteractions(articleRepository);
        }

        @Test
        @DisplayName("should fail with InvalidPrice when price is zero")
        void shouldFailWhenPriceIsZero() {
            var cmd = new CreateArticleCommand("Testprodukt", "ART-001", CATEGORY_ID,
                    Unit.PIECE_FIXED, PriceModel.FIXED, BigDecimal.ZERO);

            var result = createArticle.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPrice.class);
            verifyNoInteractions(articleRepository);
        }

        @Test
        @DisplayName("should fail with InvalidPriceModelCombination for mismatched unit/priceModel")
        void shouldFailWhenUnitPriceModelMismatch() {
            var cmd = new CreateArticleCommand("Testprodukt", "ART-001", CATEGORY_ID,
                    Unit.KG, PriceModel.FIXED, new BigDecimal("9.99"));

            var result = createArticle.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPriceModelCombination.class);
            verifyNoInteractions(articleRepository);
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when existsByArticleNumber fails")
        void shouldFailWhenExistsCheckFails() {
            when(articleRepository.existsByArticleNumber(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new CreateArticleCommand("Testprodukt", "ART-001", CATEGORY_ID,
                    Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));

            var result = createArticle.execute(cmd, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            when(articleRepository.existsByArticleNumber(any()))
                    .thenReturn(Result.success(false));
            when(articleRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new CreateArticleCommand("Testprodukt", "ART-001", CATEGORY_ID,
                    Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));

            var result = createArticle.execute(cmd, performedBy);

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

    // ==================== UpdateArticle ====================

    @Nested
    @DisplayName("UpdateArticle")
    class UpdateArticleTest {

        private UpdateArticle updateArticle;

        @BeforeEach
        void setUp() {
            lenient().when(unitOfWork.executeAtomically(any()))
                    .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
            updateArticle = new UpdateArticle(articleRepository, unitOfWork);
        }

        @Test
        @DisplayName("should update article name successfully")
        void shouldUpdateName() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.success(null));

            var cmd = new UpdateArticleCommand(articleId, "Neuer Name", null);
            var result = updateArticle.execute(cmd, performedBy);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().name().value()).isEqualTo("Neuer Name");
            verify(articleRepository).save(any());
        }

        @Test
        @DisplayName("should update category successfully")
        void shouldUpdateCategory() {
            var articleId = "article-1";
            var newCategoryId = UUID.randomUUID().toString();
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.success(null));

            var cmd = new UpdateArticleCommand(articleId, null, newCategoryId);
            var result = updateArticle.execute(cmd, performedBy);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().categoryId().value()).isEqualTo(newCategoryId);
        }

        @Test
        @DisplayName("should fail with ArticleNotFound when article does not exist")
        void shouldFailWhenNotFound() {
            var articleId = "nonexistent";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.empty()));

            var cmd = new UpdateArticleCommand(articleId, "Neuer Name", null);
            var result = updateArticle.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ArticleNotFound.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with ValidationFailure when new name is blank")
        void shouldFailWhenNewNameIsBlank() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));

            var cmd = new UpdateArticleCommand(articleId, "", null);
            var result = updateArticle.execute(cmd, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new UpdateArticleCommand(articleId, "Neuer Name", null);
            var result = updateArticle.execute(cmd, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new UpdateArticleCommand(articleId, "Neuer Name", null);
            var result = updateArticle.execute(cmd, performedBy);

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

    // ==================== GetArticle ====================

    @Nested
    @DisplayName("GetArticle")
    class GetArticleTest {

        private GetArticle getArticle;

        @BeforeEach
        void setUp() {
            getArticle = new GetArticle(articleRepository);
        }

        @Test
        @DisplayName("should return article when found")
        void shouldReturnArticleWhenFound() {
            var articleId = ArticleId.of("article-1");
            when(articleRepository.findById(articleId))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle("article-1"))));

            var result = getArticle.execute(articleId);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().id()).isEqualTo(articleId);
        }

        @Test
        @DisplayName("should fail with ArticleNotFound when not found")
        void shouldFailWhenNotFound() {
            var articleId = ArticleId.of("nonexistent");
            when(articleRepository.findById(articleId))
                    .thenReturn(Result.success(Optional.empty()));

            var result = getArticle.execute(articleId);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ArticleNotFound.class);
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when repository fails")
        void shouldFailWhenRepositoryFails() {
            var articleId = ArticleId.of("article-1");
            when(articleRepository.findById(articleId))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = getArticle.execute(articleId);

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

    // ==================== ListArticles ====================

    @Nested
    @DisplayName("ListArticles")
    class ListArticlesTest {

        private ListArticles listArticles;

        @BeforeEach
        void setUp() {
            listArticles = new ListArticles(articleRepository);
        }

        @Test
        @DisplayName("should return all articles")
        void shouldReturnAllArticles() {
            var articles = List.of(reconstitutedArticle("a-1"), reconstitutedArticle("a-2"));
            when(articleRepository.findAll()).thenReturn(Result.success(articles));

            var result = listArticles.execute();

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(2);
        }

        @Test
        @DisplayName("should return empty list when none exist")
        void shouldReturnEmptyList() {
            when(articleRepository.findAll()).thenReturn(Result.success(List.of()));

            var result = listArticles.execute();

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).isEmpty();
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findAll fails")
        void shouldFailWhenRepositoryFails() {
            when(articleRepository.findAll())
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = listArticles.execute();

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

        @Test
        @DisplayName("should return articles by category")
        void shouldReturnByCategory() {
            var catId = ProductCategoryId.of(CATEGORY_ID);
            var articles = List.of(reconstitutedArticle("a-1"));
            when(articleRepository.findByCategory(catId)).thenReturn(Result.success(articles));

            var result = listArticles.executeByCategory(catId);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(1);
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findByCategory fails")
        void shouldFailWhenFindByCategoryFails() {
            var catId = ProductCategoryId.of(CATEGORY_ID);
            when(articleRepository.findByCategory(catId))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = listArticles.executeByCategory(catId);

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

        @Test
        @DisplayName("should return articles by status")
        void shouldReturnByStatus() {
            var articles = List.of(reconstitutedArticle("a-1"));
            when(articleRepository.findByStatus(ArticleStatus.ACTIVE))
                    .thenReturn(Result.success(articles));

            var result = listArticles.executeByStatus(ArticleStatus.ACTIVE);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(1);
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findByStatus fails")
        void shouldFailWhenFindByStatusFails() {
            when(articleRepository.findByStatus(ArticleStatus.ACTIVE))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = listArticles.executeByStatus(ArticleStatus.ACTIVE);

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

    // ==================== ActivateArticle ====================

    @Nested
    @DisplayName("ActivateArticle")
    class ActivateArticleTest {

        private ActivateArticle activateArticle;

        @BeforeEach
        void setUp() {
            lenient().when(unitOfWork.executeAtomically(any()))
                    .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
            activateArticle = new ActivateArticle(articleRepository, unitOfWork);
        }

        @Test
        @DisplayName("should activate article successfully")
        void shouldActivateArticle() {
            var articleId = ArticleId.of("article-1");
            var article = Article.reconstitute(
                    articleId, new ArticleName("Test"), new ArticleNumber("ART-001"),
                    ProductCategoryId.of(CATEGORY_ID),
                    new ArrayList<>(List.of(SalesUnit.reconstitute(SalesUnitId.of("su-1"),
                            Unit.PIECE_FIXED, PriceModel.FIXED, Money.euro(new BigDecimal("9.99"))))),
                    ArticleStatus.INACTIVE, new HashSet<>(), NOW, NOW
            );
            when(articleRepository.findById(articleId))
                    .thenReturn(Result.success(Optional.of(article)));
            when(articleRepository.save(any()))
                    .thenReturn(Result.success(null));

            var result = activateArticle.execute(articleId, performedBy);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().status()).isEqualTo(ArticleStatus.ACTIVE);
            verify(articleRepository).save(any());
        }

        @Test
        @DisplayName("should fail with ArticleNotFound when article does not exist")
        void shouldFailWhenNotFound() {
            var articleId = ArticleId.of("nonexistent");
            when(articleRepository.findById(articleId))
                    .thenReturn(Result.success(Optional.empty()));

            var result = activateArticle.execute(articleId, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ArticleNotFound.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var articleId = ArticleId.of("article-1");
            when(articleRepository.findById(articleId))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = activateArticle.execute(articleId, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var articleId = ArticleId.of("article-1");
            when(articleRepository.findById(articleId))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle("article-1"))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = activateArticle.execute(articleId, performedBy);

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

    // ==================== DeactivateArticle ====================

    @Nested
    @DisplayName("DeactivateArticle")
    class DeactivateArticleTest {

        private DeactivateArticle deactivateArticle;

        @BeforeEach
        void setUp() {
            lenient().when(unitOfWork.executeAtomically(any()))
                    .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
            deactivateArticle = new DeactivateArticle(articleRepository, unitOfWork);
        }

        @Test
        @DisplayName("should deactivate article successfully")
        void shouldDeactivateArticle() {
            var articleId = ArticleId.of("article-1");
            when(articleRepository.findById(articleId))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle("article-1"))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.success(null));

            var result = deactivateArticle.execute(articleId, performedBy);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().status()).isEqualTo(ArticleStatus.INACTIVE);
            verify(articleRepository).save(any());
        }

        @Test
        @DisplayName("should fail with ArticleNotFound when article does not exist")
        void shouldFailWhenNotFound() {
            var articleId = ArticleId.of("nonexistent");
            when(articleRepository.findById(articleId))
                    .thenReturn(Result.success(Optional.empty()));

            var result = deactivateArticle.execute(articleId, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ArticleNotFound.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var articleId = ArticleId.of("article-1");
            when(articleRepository.findById(articleId))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = deactivateArticle.execute(articleId, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var articleId = ArticleId.of("article-1");
            when(articleRepository.findById(articleId))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle("article-1"))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = deactivateArticle.execute(articleId, performedBy);

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

    // ==================== AddSalesUnit ====================

    @Nested
    @DisplayName("AddSalesUnit")
    class AddSalesUnitTest {

        private AddSalesUnit addSalesUnit;

        @BeforeEach
        void setUp() {
            lenient().when(unitOfWork.executeAtomically(any()))
                    .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
            addSalesUnit = new AddSalesUnit(articleRepository, unitOfWork);
        }

        @Test
        @DisplayName("should add sales unit successfully")
        void shouldAddSalesUnit() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.success(null));

            var cmd = new AddSalesUnitCommand(articleId, Unit.KG, PriceModel.WEIGHT_BASED,
                    new BigDecimal("19.99"));
            var result = addSalesUnit.execute(cmd, performedBy);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().salesUnits()).hasSize(2);
            verify(articleRepository).save(any());
        }

        @Test
        @DisplayName("should fail with ArticleNotFound when article does not exist")
        void shouldFailWhenNotFound() {
            var articleId = "nonexistent";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.empty()));

            var cmd = new AddSalesUnitCommand(articleId, Unit.KG, PriceModel.WEIGHT_BASED,
                    new BigDecimal("19.99"));
            var result = addSalesUnit.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ArticleNotFound.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with DuplicateSalesUnitType when unit type already exists")
        void shouldFailWhenDuplicateUnitType() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));

            var cmd = new AddSalesUnitCommand(articleId, Unit.PIECE_FIXED, PriceModel.FIXED,
                    new BigDecimal("5.00"));
            var result = addSalesUnit.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.DuplicateSalesUnitType.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with ValidationFailure when price is negative")
        void shouldFailWhenPriceIsNegative() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));

            var cmd = new AddSalesUnitCommand(articleId, Unit.KG, PriceModel.WEIGHT_BASED,
                    new BigDecimal("-1.00"));
            var result = addSalesUnit.execute(cmd, performedBy);

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

        @Test
        @DisplayName("should fail with InvalidPriceModelCombination for mismatched unit/priceModel")
        void shouldFailWhenUnitPriceModelMismatch() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));

            var cmd = new AddSalesUnitCommand(articleId, Unit.KG, PriceModel.FIXED,
                    new BigDecimal("19.99"));
            var result = addSalesUnit.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPriceModelCombination.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new AddSalesUnitCommand(articleId, Unit.KG, PriceModel.WEIGHT_BASED,
                    new BigDecimal("19.99"));
            var result = addSalesUnit.execute(cmd, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new AddSalesUnitCommand(articleId, Unit.KG, PriceModel.WEIGHT_BASED,
                    new BigDecimal("19.99"));
            var result = addSalesUnit.execute(cmd, performedBy);

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

    // ==================== RemoveSalesUnit ====================

    @Nested
    @DisplayName("RemoveSalesUnit")
    class RemoveSalesUnitTest {

        private RemoveSalesUnit removeSalesUnit;

        @BeforeEach
        void setUp() {
            lenient().when(unitOfWork.executeAtomically(any()))
                    .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
            removeSalesUnit = new RemoveSalesUnit(articleRepository, unitOfWork);
        }

        @Test
        @DisplayName("should remove sales unit successfully when article has multiple units")
        void shouldRemoveSalesUnit() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticleWithTwoUnits(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.success(null));

            var cmd = new RemoveSalesUnitCommand(articleId, "su-2");
            var result = removeSalesUnit.execute(cmd, performedBy);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().salesUnits()).hasSize(1);
            verify(articleRepository).save(any());
        }

        @Test
        @DisplayName("should fail with MinimumSalesUnitRequired when only one unit remains")
        void shouldFailWhenOnlyOneUnit() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));

            var cmd = new RemoveSalesUnitCommand(articleId, "su-1");
            var result = removeSalesUnit.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.MinimumSalesUnitRequired.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with SalesUnitNotFound when sales unit does not exist")
        void shouldFailWhenSalesUnitNotFound() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticleWithTwoUnits(articleId))));

            var cmd = new RemoveSalesUnitCommand(articleId, "nonexistent-su");
            var result = removeSalesUnit.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.SalesUnitNotFound.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with ArticleNotFound when article does not exist")
        void shouldFailWhenArticleNotFound() {
            var articleId = "nonexistent";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.empty()));

            var cmd = new RemoveSalesUnitCommand(articleId, "su-1");
            var result = removeSalesUnit.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ArticleNotFound.class);
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new RemoveSalesUnitCommand(articleId, "su-1");
            var result = removeSalesUnit.execute(cmd, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticleWithTwoUnits(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new RemoveSalesUnitCommand(articleId, "su-2");
            var result = removeSalesUnit.execute(cmd, performedBy);

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

    // ==================== UpdateSalesUnitPrice ====================

    @Nested
    @DisplayName("UpdateSalesUnitPrice")
    class UpdateSalesUnitPriceTest {

        private UpdateSalesUnitPrice updateSalesUnitPrice;

        @BeforeEach
        void setUp() {
            lenient().when(unitOfWork.executeAtomically(any()))
                    .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
            updateSalesUnitPrice = new UpdateSalesUnitPrice(articleRepository, unitOfWork);
        }

        @Test
        @DisplayName("should update sales unit price successfully")
        void shouldUpdatePrice() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.success(null));

            var cmd = new UpdateSalesUnitPriceCommand(articleId, "su-1", new BigDecimal("14.99"));
            var result = updateSalesUnitPrice.execute(cmd, performedBy);

            assertThat(result.isSuccess()).isTrue();
            var salesUnit = result.unsafeGetValue().salesUnits().stream()
                    .filter(su -> su.id().value().equals("su-1"))
                    .findFirst().orElseThrow();
            assertThat(salesUnit.price().amount()).isEqualByComparingTo(new BigDecimal("14.99"));
            verify(articleRepository).save(any());
        }

        @Test
        @DisplayName("should fail with ArticleNotFound when article does not exist")
        void shouldFailWhenNotFound() {
            var articleId = "nonexistent";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.empty()));

            var cmd = new UpdateSalesUnitPriceCommand(articleId, "su-1", new BigDecimal("14.99"));
            var result = updateSalesUnitPrice.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ArticleNotFound.class);
        }

        @Test
        @DisplayName("should fail with SalesUnitNotFound when sales unit does not exist")
        void shouldFailWhenSalesUnitNotFound() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));

            var cmd = new UpdateSalesUnitPriceCommand(articleId, "nonexistent-su", new BigDecimal("14.99"));
            var result = updateSalesUnitPrice.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.SalesUnitNotFound.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with ValidationFailure when price is negative")
        void shouldFailWhenPriceIsNegative() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));

            var cmd = new UpdateSalesUnitPriceCommand(articleId, "su-1", new BigDecimal("-5.00"));
            var result = updateSalesUnitPrice.execute(cmd, performedBy);

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

        @Test
        @DisplayName("should fail with InvalidPrice when price is zero")
        void shouldFailWhenPriceIsZero() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));

            var cmd = new UpdateSalesUnitPriceCommand(articleId, "su-1", BigDecimal.ZERO);
            var result = updateSalesUnitPrice.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPrice.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new UpdateSalesUnitPriceCommand(articleId, "su-1", new BigDecimal("14.99"));
            var result = updateSalesUnitPrice.execute(cmd, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new UpdateSalesUnitPriceCommand(articleId, "su-1", new BigDecimal("14.99"));
            var result = updateSalesUnitPrice.execute(cmd, performedBy);

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

    // ==================== AssignSupplier ====================

    @Nested
    @DisplayName("AssignSupplier")
    class AssignSupplierTest {

        private AssignSupplier assignSupplier;

        @BeforeEach
        void setUp() {
            lenient().when(unitOfWork.executeAtomically(any()))
                    .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
            assignSupplier = new AssignSupplier(articleRepository, unitOfWork);
        }

        @Test
        @DisplayName("should assign supplier successfully")
        void shouldAssignSupplier() {
            var articleId = "article-1";
            var supplierId = UUID.randomUUID().toString();
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.success(null));

            var cmd = new AssignSupplierCommand(articleId, supplierId);
            var result = assignSupplier.execute(cmd, performedBy);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().supplierReferences())
                    .contains(SupplierId.of(supplierId));
            verify(articleRepository).save(any());
        }

        @Test
        @DisplayName("should fail with ArticleNotFound when article does not exist")
        void shouldFailWhenNotFound() {
            var articleId = "nonexistent";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.empty()));

            var cmd = new AssignSupplierCommand(articleId, UUID.randomUUID().toString());
            var result = assignSupplier.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ArticleNotFound.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new AssignSupplierCommand(articleId, UUID.randomUUID().toString());
            var result = assignSupplier.execute(cmd, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new AssignSupplierCommand(articleId, UUID.randomUUID().toString());
            var result = assignSupplier.execute(cmd, performedBy);

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

    // ==================== RemoveSupplier ====================

    @Nested
    @DisplayName("RemoveSupplier")
    class RemoveSupplierTest {

        private RemoveSupplier removeSupplier;

        @BeforeEach
        void setUp() {
            lenient().when(unitOfWork.executeAtomically(any()))
                    .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
            removeSupplier = new RemoveSupplier(articleRepository, unitOfWork);
        }

        @Test
        @DisplayName("should remove supplier successfully")
        void shouldRemoveSupplier() {
            var articleId = "article-1";
            var supplierId = UUID.randomUUID().toString();
            var article = reconstitutedArticle(articleId);
            article.addSupplierReference(SupplierId.of(supplierId));

            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(article)));
            when(articleRepository.save(any()))
                    .thenReturn(Result.success(null));

            var cmd = new RemoveSupplierCommand(articleId, supplierId);
            var result = removeSupplier.execute(cmd, performedBy);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().supplierReferences())
                    .doesNotContain(SupplierId.of(supplierId));
            verify(articleRepository).save(any());
        }

        @Test
        @DisplayName("should succeed even when supplier was not assigned (idempotent)")
        void shouldSucceedWhenSupplierNotAssigned() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.success(null));

            var cmd = new RemoveSupplierCommand(articleId, UUID.randomUUID().toString());
            var result = removeSupplier.execute(cmd, performedBy);

            assertThat(result.isSuccess()).isTrue();
            verify(articleRepository).save(any());
        }

        @Test
        @DisplayName("should fail with ArticleNotFound when article does not exist")
        void shouldFailWhenNotFound() {
            var articleId = "nonexistent";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.empty()));

            var cmd = new RemoveSupplierCommand(articleId, UUID.randomUUID().toString());
            var result = removeSupplier.execute(cmd, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ArticleNotFound.class);
            verify(articleRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new RemoveSupplierCommand(articleId, UUID.randomUUID().toString());
            var result = removeSupplier.execute(cmd, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var articleId = "article-1";
            when(articleRepository.findById(ArticleId.of(articleId)))
                    .thenReturn(Result.success(Optional.of(reconstitutedArticle(articleId))));
            when(articleRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var cmd = new RemoveSupplierCommand(articleId, UUID.randomUUID().toString());
            var result = removeSupplier.execute(cmd, performedBy);

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