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 java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

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


class InventoryCountTest {

    // ==================== Create ====================

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

        @Test
        @DisplayName("should create InventoryCount with valid inputs")
        void shouldCreateWithValidInputs() {
            var draft = new InventoryCountDraft("location-1", LocalDate.now().toString(), "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isSuccess()).isTrue();
            var count = result.unsafeGetValue();
            assertThat(count.id()).isNotNull();
            assertThat(count.storageLocationId().value()).isEqualTo("location-1");
            assertThat(count.countDate()).isEqualTo(LocalDate.now());
            assertThat(count.initiatedBy()).isEqualTo("user-1");
            assertThat(count.status()).isEqualTo(InventoryCountStatus.OPEN);
            assertThat(count.countItems()).isEmpty();
        }

        @Test
        @DisplayName("should create InventoryCount with past date")
        void shouldCreateWithPastDate() {
            var draft = new InventoryCountDraft("location-1", "2025-01-15", "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().countDate()).isEqualTo(LocalDate.of(2025, 1, 15));
        }

        @Test
        @DisplayName("should fail when storageLocationId is null")
        void shouldFailWhenStorageLocationIdNull() {
            var draft = new InventoryCountDraft(null, LocalDate.now().toString(), "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStorageLocationId.class);
        }

        @Test
        @DisplayName("should fail when storageLocationId is blank")
        void shouldFailWhenStorageLocationIdBlank() {
            var draft = new InventoryCountDraft("  ", LocalDate.now().toString(), "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStorageLocationId.class);
        }

        @Test
        @DisplayName("should fail when countDate is null")
        void shouldFailWhenCountDateNull() {
            var draft = new InventoryCountDraft("location-1", null, "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidCountDate.class);
        }

        @Test
        @DisplayName("should fail when countDate is invalid")
        void shouldFailWhenCountDateInvalid() {
            var draft = new InventoryCountDraft("location-1", "not-a-date", "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidCountDate.class);
        }

        @Test
        @DisplayName("should fail when countDate is in the future")
        void shouldFailWhenCountDateInFuture() {
            var futureDate = LocalDate.now().plusDays(1).toString();
            var draft = new InventoryCountDraft("location-1", futureDate, "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.CountDateInFuture.class);
        }

        @Test
        @DisplayName("should fail when initiatedBy is null")
        void shouldFailWhenInitiatedByNull() {
            var draft = new InventoryCountDraft("location-1", LocalDate.now().toString(), null);

            var result = InventoryCount.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInitiatedBy.class);
        }

        @Test
        @DisplayName("should fail when initiatedBy is blank")
        void shouldFailWhenInitiatedByBlank() {
            var draft = new InventoryCountDraft("location-1", LocalDate.now().toString(), "  ");

            var result = InventoryCount.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInitiatedBy.class);
        }

        @Test
        @DisplayName("should fail when storageLocationId is empty string")
        void shouldFailWhenStorageLocationIdEmpty() {
            var draft = new InventoryCountDraft("", LocalDate.now().toString(), "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStorageLocationId.class);
        }

        @Test
        @DisplayName("should fail when initiatedBy is empty string")
        void shouldFailWhenInitiatedByEmpty() {
            var draft = new InventoryCountDraft("location-1", LocalDate.now().toString(), "");

            var result = InventoryCount.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInitiatedBy.class);
        }

        @Test
        @DisplayName("should fail when countDate is blank")
        void shouldFailWhenCountDateBlank() {
            var draft = new InventoryCountDraft("location-1", "  ", "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidCountDate.class);
        }

        @Test
        @DisplayName("should accept today as countDate (boundary)")
        void shouldAcceptTodayAsCountDate() {
            var draft = new InventoryCountDraft("location-1", LocalDate.now().toString(), "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().countDate()).isEqualTo(LocalDate.now());
        }

        @Test
        @DisplayName("should fail with invalid date like 2025-13-01")
        void shouldFailWhenDateHasInvalidMonth() {
            var draft = new InventoryCountDraft("location-1", "2025-13-01", "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidCountDate.class);
        }

        @Test
        @DisplayName("should fail with invalid date like 2025-02-30")
        void shouldFailWhenDateHasInvalidDay() {
            var draft = new InventoryCountDraft("location-1", "2025-02-30", "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidCountDate.class);
        }

        @Test
        @DisplayName("should accept leap year date 2024-02-29")
        void shouldAcceptLeapYearDate() {
            var draft = new InventoryCountDraft("location-1", "2024-02-29", "user-1");

            var result = InventoryCount.create(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().countDate()).isEqualTo(LocalDate.of(2024, 2, 29));
        }
    }

    // ==================== addCountItem ====================

    @Nested
    @DisplayName("addCountItem()")
    class AddCountItem {

        @Test
        @DisplayName("should add count item to OPEN count")
        void shouldAddCountItemToOpenCount() {
            var count = createOpenCount();
            var itemDraft = new CountItemDraft("article-1", "10.0", "KILOGRAM");

            var result = count.addCountItem(itemDraft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(count.countItems()).hasSize(1);
            assertThat(result.unsafeGetValue().articleId().value()).isEqualTo("article-1");
        }

        @Test
        @DisplayName("should add multiple count items with different articles")
        void shouldAddMultipleCountItems() {
            var count = createOpenCount();

            count.addCountItem(new CountItemDraft("article-1", "10.0", "KILOGRAM"));
            count.addCountItem(new CountItemDraft("article-2", "5.0", "LITER"));

            assertThat(count.countItems()).hasSize(2);
        }

        @Test
        @DisplayName("should fail when adding duplicate articleId")
        void shouldFailWhenDuplicateArticle() {
            var count = createOpenCount();
            count.addCountItem(new CountItemDraft("article-1", "10.0", "KILOGRAM"));

            var result = count.addCountItem(new CountItemDraft("article-1", "5.0", "KILOGRAM"));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.DuplicateArticle.class);
        }

        @Test
        @DisplayName("should fail when count is not OPEN")
        void shouldFailWhenNotOpen() {
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"),
                    StorageLocationId.of("location-1"),
                    LocalDate.now(),
                    "user-1",
                    null,
                    InventoryCountStatus.COUNTING,
                    null,
                    java.time.Instant.now(),
                    java.util.List.of()
            );

            var result = count.addCountItem(new CountItemDraft("article-1", "10.0", "KILOGRAM"));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when articleId is blank")
        void shouldFailWhenArticleIdBlank() {
            var count = createOpenCount();

            var result = count.addCountItem(new CountItemDraft("", "10.0", "KILOGRAM"));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidArticleId.class);
        }

        @Test
        @DisplayName("should fail when quantity amount is invalid")
        void shouldFailWhenQuantityInvalid() {
            var count = createOpenCount();

            var result = count.addCountItem(new CountItemDraft("article-1", "not-a-number", "KILOGRAM"));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidQuantity.class);
        }

        @Test
        @DisplayName("should fail when unit is invalid")
        void shouldFailWhenUnitInvalid() {
            var count = createOpenCount();

            var result = count.addCountItem(new CountItemDraft("article-1", "10.0", "INVALID_UNIT"));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidQuantity.class);
        }

        @Test
        @DisplayName("should allow zero expected quantity")
        void shouldAllowZeroExpectedQuantity() {
            var count = createOpenCount();

            var result = count.addCountItem(new CountItemDraft("article-1", "0", "KILOGRAM"));

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

        @Test
        @DisplayName("should fail when count is COMPLETED")
        void shouldFailWhenCompleted() {
            var count = reconstitute(InventoryCountStatus.COMPLETED);

            var result = count.addCountItem(new CountItemDraft("article-1", "10.0", "KILOGRAM"));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when count is CANCELLED")
        void shouldFailWhenCancelled() {
            var count = reconstitute(InventoryCountStatus.CANCELLED);

            var result = count.addCountItem(new CountItemDraft("article-1", "10.0", "KILOGRAM"));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when quantity is negative")
        void shouldFailWhenNegativeQuantity() {
            var count = createOpenCount();

            var result = count.addCountItem(new CountItemDraft("article-1", "-5.0", "KILOGRAM"));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidQuantity.class);
        }

        @Test
        @DisplayName("should fail when articleId is null")
        void shouldFailWhenArticleIdNull() {
            var count = createOpenCount();

            var result = count.addCountItem(new CountItemDraft(null, "10.0", "KILOGRAM"));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidArticleId.class);
        }

        @Test
        @DisplayName("should fail when quantity amount is null")
        void shouldFailWhenQuantityAmountNull() {
            var count = createOpenCount();

            var result = count.addCountItem(new CountItemDraft("article-1", null, "KILOGRAM"));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidQuantity.class);
        }

        @Test
        @DisplayName("should fail when unit is null")
        void shouldFailWhenUnitNull() {
            var count = createOpenCount();

            var result = count.addCountItem(new CountItemDraft("article-1", "10.0", null));

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidQuantity.class);
        }

        private InventoryCount reconstitute(InventoryCountStatus status) {
            return InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.now(), "user-1", null, status,
                    null, Instant.now(), List.of()
            );
        }
    }

    // ==================== startCounting ====================

    @Nested
    @DisplayName("startCounting()")
    class StartCounting {

        @Test
        @DisplayName("should transition from OPEN to COUNTING when countItems exist")
        void shouldTransitionToCountingWhenItemsExist() {
            var count = createOpenCount();
            count.addCountItem(new CountItemDraft("article-1", "10.0", "KILOGRAM"));

            var result = count.startCounting();

            assertThat(result.isSuccess()).isTrue();
            assertThat(count.status()).isEqualTo(InventoryCountStatus.COUNTING);
        }

        @Test
        @DisplayName("should fail when status is COUNTING")
        void shouldFailWhenStatusIsCounting() {
            var count = createCountingCount();

            var result = count.startCounting();

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when status is COMPLETED")
        void shouldFailWhenStatusIsCompleted() {
            var count = reconstitute(InventoryCountStatus.COMPLETED, createCountItems());

            var result = count.startCounting();

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when status is CANCELLED")
        void shouldFailWhenStatusIsCancelled() {
            var count = reconstitute(InventoryCountStatus.CANCELLED, createCountItems());

            var result = count.startCounting();

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when countItems is empty")
        void shouldFailWhenNoCountItems() {
            var count = createOpenCount();

            var result = count.startCounting();

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.NoCountItems.class);
        }

        private InventoryCount reconstitute(InventoryCountStatus status, List<CountItem> items) {
            return InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.now(), "user-1", null, status,
                    null, Instant.now(), items
            );
        }
    }

    // ==================== updateCountItem ====================

    @Nested
    @DisplayName("updateCountItem()")
    class UpdateCountItem {

        @Test
        @DisplayName("should record actual quantity on count item")
        void shouldRecordActualQuantity() {
            var count = createCountingCount();
            var itemId = count.countItems().getFirst().id();
            var actualQty = Quantity.reconstitute(new BigDecimal("8.0"), UnitOfMeasure.KILOGRAM);

            var result = count.updateCountItem(itemId, actualQty);

            assertThat(result.isSuccess()).isTrue();
            assertThat(count.countItems().getFirst().actualQuantity()).isEqualTo(actualQty);
            assertThat(count.countItems().getFirst().isCounted()).isTrue();
        }

        @Test
        @DisplayName("should allow zero actual quantity")
        void shouldAllowZeroActualQuantity() {
            var count = createCountingCount();
            var itemId = count.countItems().getFirst().id();
            var actualQty = Quantity.reconstitute(BigDecimal.ZERO, UnitOfMeasure.KILOGRAM);

            var result = count.updateCountItem(itemId, actualQty);

            assertThat(result.isSuccess()).isTrue();
            assertThat(count.countItems().getFirst().actualQuantity().amount())
                    .isEqualByComparingTo(BigDecimal.ZERO);
        }

        @Test
        @DisplayName("should compute deviation after recording")
        void shouldComputeDeviationAfterRecording() {
            var count = createCountingCount();
            var itemId = count.countItems().getFirst().id();
            var actualQty = Quantity.reconstitute(new BigDecimal("7.0"), UnitOfMeasure.KILOGRAM);

            count.updateCountItem(itemId, actualQty);

            assertThat(count.countItems().getFirst().deviation())
                    .isEqualByComparingTo(new BigDecimal("-3.0"));
        }

        @Test
        @DisplayName("should fail when status is OPEN")
        void shouldFailWhenStatusIsOpen() {
            var count = createOpenCount();
            count.addCountItem(new CountItemDraft("article-1", "10.0", "KILOGRAM"));
            var itemId = count.countItems().getFirst().id();
            var actualQty = Quantity.reconstitute(new BigDecimal("8.0"), UnitOfMeasure.KILOGRAM);

            var result = count.updateCountItem(itemId, actualQty);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when status is COMPLETED")
        void shouldFailWhenStatusIsCompleted() {
            var items = createCountItems();
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.COMPLETED,
                    null, Instant.now(), items
            );
            var actualQty = Quantity.reconstitute(new BigDecimal("8.0"), UnitOfMeasure.KILOGRAM);

            var result = count.updateCountItem(items.getFirst().id(), actualQty);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when countItem not found")
        void shouldFailWhenCountItemNotFound() {
            var count = createCountingCount();
            var actualQty = Quantity.reconstitute(new BigDecimal("8.0"), UnitOfMeasure.KILOGRAM);

            var result = count.updateCountItem(CountItemId.of("nonexistent"), actualQty);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.CountItemNotFound.class);
        }

        @Test
        @DisplayName("should fail when UOM does not match expected quantity")
        void shouldFailWhenUomMismatch() {
            var count = createCountingCount();
            var itemId = count.countItems().getFirst().id();
            var actualQty = Quantity.reconstitute(new BigDecimal("8.0"), UnitOfMeasure.LITER);

            var result = count.updateCountItem(itemId, actualQty);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidQuantity.class);
        }

        @Test
        @DisplayName("should fail when actual quantity is negative")
        void shouldFailWhenNegativeQuantity() {
            var count = createCountingCount();
            var itemId = count.countItems().getFirst().id();
            var actualQty = Quantity.reconstitute(new BigDecimal("-1.0"), UnitOfMeasure.KILOGRAM);

            var result = count.updateCountItem(itemId, actualQty);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidQuantity.class);
        }

        @Test
        @DisplayName("should allow overwriting previously recorded quantity")
        void shouldAllowOverwritingRecordedQuantity() {
            var count = createCountingCount();
            var itemId = count.countItems().getFirst().id();

            count.updateCountItem(itemId, Quantity.reconstitute(new BigDecimal("8.0"), UnitOfMeasure.KILOGRAM));
            var result = count.updateCountItem(itemId, Quantity.reconstitute(new BigDecimal("12.0"), UnitOfMeasure.KILOGRAM));

            assertThat(result.isSuccess()).isTrue();
            assertThat(count.countItems().getFirst().actualQuantity().amount())
                    .isEqualByComparingTo(new BigDecimal("12.0"));
        }

        @Test
        @DisplayName("should update specific item in multi-item count")
        void shouldUpdateSpecificItemInMultiItemCount() {
            var items = List.of(
                    CountItem.reconstitute(CountItemId.of("item-1"), ArticleId.of("article-1"),
                            Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM), null),
                    CountItem.reconstitute(CountItemId.of("item-2"), ArticleId.of("article-2"),
                            Quantity.reconstitute(new BigDecimal("5.0"), UnitOfMeasure.LITER), null)
            );
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.COUNTING,
                    null, Instant.now(), items
            );
            var actualQty = Quantity.reconstitute(new BigDecimal("3.0"), UnitOfMeasure.LITER);

            var result = count.updateCountItem(CountItemId.of("item-2"), actualQty);

            assertThat(result.isSuccess()).isTrue();
            assertThat(count.countItems().get(1).actualQuantity()).isEqualTo(actualQty);
            assertThat(count.countItems().getFirst().actualQuantity()).isNull();
        }
    }

    // ==================== CountItem deviation ====================

    @Nested
    @DisplayName("CountItem deviation()")
    class Deviation {

        @Test
        @DisplayName("should return null when not yet counted")
        void shouldReturnNullWhenNotCounted() {
            var item = CountItem.create(new CountItemDraft("article-1", "10.0", "KILOGRAM")).unsafeGetValue();

            assertThat(item.deviation()).isNull();
            assertThat(item.isCounted()).isFalse();
        }

        @Test
        @DisplayName("should compute negative deviation (actual < expected)")
        void shouldComputeNegativeDeviation() {
            var item = CountItem.reconstitute(
                    CountItemId.of("item-1"),
                    ArticleId.of("article-1"),
                    Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM),
                    Quantity.reconstitute(new BigDecimal("8.0"), UnitOfMeasure.KILOGRAM)
            );

            assertThat(item.isCounted()).isTrue();
            assertThat(item.deviation()).isEqualByComparingTo(new BigDecimal("-2.0"));
        }

        @Test
        @DisplayName("should compute positive deviation (actual > expected)")
        void shouldComputePositiveDeviation() {
            var item = CountItem.reconstitute(
                    CountItemId.of("item-1"),
                    ArticleId.of("article-1"),
                    Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM),
                    Quantity.reconstitute(new BigDecimal("12.5"), UnitOfMeasure.KILOGRAM)
            );

            assertThat(item.isCounted()).isTrue();
            assertThat(item.deviation()).isEqualByComparingTo(new BigDecimal("2.5"));
        }

        @Test
        @DisplayName("should return null when UOMs differ")
        void shouldReturnNullWhenUomsDiffer() {
            var item = CountItem.reconstitute(
                    CountItemId.of("item-1"),
                    ArticleId.of("article-1"),
                    Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM),
                    Quantity.reconstitute(new BigDecimal("8.0"), UnitOfMeasure.LITER)
            );

            assertThat(item.isCounted()).isTrue();
            assertThat(item.deviation()).isNull();
        }

        @Test
        @DisplayName("should compute zero deviation (actual == expected)")
        void shouldComputeZeroDeviation() {
            var item = CountItem.reconstitute(
                    CountItemId.of("item-1"),
                    ArticleId.of("article-1"),
                    Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM),
                    Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM)
            );

            assertThat(item.isCounted()).isTrue();
            assertThat(item.deviation()).isEqualByComparingTo(BigDecimal.ZERO);
        }
    }

    // ==================== isActive ====================

    @Nested
    @DisplayName("isActive()")
    class IsActive {

        @Test
        @DisplayName("OPEN should be active")
        void openShouldBeActive() {
            var count = createOpenCount();
            assertThat(count.isActive()).isTrue();
        }

        @Test
        @DisplayName("COUNTING should be active")
        void countingShouldBeActive() {
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("c1"), StorageLocationId.of("l1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.COUNTING,
                    null, Instant.now(), List.of()
            );
            assertThat(count.isActive()).isTrue();
        }

        @Test
        @DisplayName("COMPLETED should not be active")
        void completedShouldNotBeActive() {
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("c1"), StorageLocationId.of("l1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.COMPLETED,
                    null, Instant.now(), List.of()
            );
            assertThat(count.isActive()).isFalse();
        }

        @Test
        @DisplayName("CANCELLED should not be active")
        void cancelledShouldNotBeActive() {
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("c1"), StorageLocationId.of("l1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.CANCELLED,
                    null, Instant.now(), List.of()
            );
            assertThat(count.isActive()).isFalse();
        }
    }

    // ==================== reconstitute ====================

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

        @Test
        @DisplayName("should reconstitute with countItems")
        void shouldReconstituteWithCountItems() {
            var items = List.of(
                    CountItem.reconstitute(
                            CountItemId.of("item-1"), ArticleId.of("article-1"),
                            Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM), null
                    ),
                    CountItem.reconstitute(
                            CountItemId.of("item-2"), ArticleId.of("article-2"),
                            Quantity.reconstitute(new BigDecimal("5.0"), UnitOfMeasure.LITER), null
                    )
            );

            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.of(2025, 6, 15), "user-1", null, InventoryCountStatus.COUNTING,
                    null, Instant.now(), items
            );

            assertThat(count.id().value()).isEqualTo("count-1");
            assertThat(count.status()).isEqualTo(InventoryCountStatus.COUNTING);
            assertThat(count.countItems()).hasSize(2);
        }

        @Test
        @DisplayName("countItems should be unmodifiable")
        void countItemsShouldBeUnmodifiable() {
            var count = createOpenCount();

            assertThatThrownBy(() -> count.countItems().add(
                    CountItem.reconstitute(
                            CountItemId.of("item-1"), ArticleId.of("article-1"),
                            Quantity.reconstitute(BigDecimal.TEN, UnitOfMeasure.KILOGRAM), null
                    )
            )).isInstanceOf(UnsupportedOperationException.class);
        }
    }

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

    @Nested
    @DisplayName("equals() / hashCode()")
    class EqualsAndHashCode {

        @Test
        @DisplayName("same id → equal")
        void sameIdShouldBeEqual() {
            var a = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("loc-a"),
                    LocalDate.now(), "user-a", null, InventoryCountStatus.OPEN,
                    null, Instant.now(), List.of()
            );
            var b = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("loc-b"),
                    LocalDate.now().minusDays(1), "user-b", null, InventoryCountStatus.COMPLETED,
                    null, Instant.now(), List.of()
            );

            assertThat(a).isEqualTo(b);
            assertThat(a.hashCode()).isEqualTo(b.hashCode());
        }

        @Test
        @DisplayName("different id → not equal")
        void differentIdShouldNotBeEqual() {
            var a = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("loc-1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.OPEN,
                    null, Instant.now(), List.of()
            );
            var b = InventoryCount.reconstitute(
                    InventoryCountId.of("count-2"), StorageLocationId.of("loc-1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.OPEN,
                    null, Instant.now(), List.of()
            );

            assertThat(a).isNotEqualTo(b);
        }
    }

    // ==================== complete ====================

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

        @Test
        @DisplayName("should transition from COUNTING to COMPLETED")
        void shouldCompleteWhenValid() {
            var count = createCountingCountAllCounted();

            var result = count.complete("user-2");

            assertThat(result.isSuccess()).isTrue();
            assertThat(count.status()).isEqualTo(InventoryCountStatus.COMPLETED);
            assertThat(count.completedBy()).isEqualTo("user-2");
        }

        @Test
        @DisplayName("should not be active after completion")
        void shouldNotBeActiveAfterCompletion() {
            var count = createCountingCountAllCounted();

            count.complete("user-2");

            assertThat(count.isActive()).isFalse();
        }

        @Test
        @DisplayName("should complete with multiple counted items")
        void shouldCompleteWithMultipleItems() {
            var items = new ArrayList<>(List.of(
                    CountItem.reconstitute(CountItemId.of("item-1"), ArticleId.of("article-1"),
                            Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM),
                            Quantity.reconstitute(new BigDecimal("8.0"), UnitOfMeasure.KILOGRAM)),
                    CountItem.reconstitute(CountItemId.of("item-2"), ArticleId.of("article-2"),
                            Quantity.reconstitute(new BigDecimal("5.0"), UnitOfMeasure.LITER),
                            Quantity.reconstitute(new BigDecimal("5.0"), UnitOfMeasure.LITER))
            ));
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.COUNTING,
                    null, Instant.now(), items
            );

            var result = count.complete("user-2");

            assertThat(result.isSuccess()).isTrue();
            assertThat(count.status()).isEqualTo(InventoryCountStatus.COMPLETED);
        }

        @Test
        @DisplayName("should fail when status is OPEN")
        void shouldFailWhenStatusIsOpen() {
            var count = createOpenCount();
            count.addCountItem(new CountItemDraft("article-1", "10.0", "KILOGRAM"));

            var result = count.complete("user-2");

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when status is COMPLETED")
        void shouldFailWhenStatusIsCompleted() {
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.now(), "user-1", "user-2", InventoryCountStatus.COMPLETED,
                    null, Instant.now(), createCountedItems()
            );

            var result = count.complete("user-3");

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when status is CANCELLED")
        void shouldFailWhenStatusIsCancelled() {
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.CANCELLED,
                    null, Instant.now(), createCountedItems()
            );

            var result = count.complete("user-2");

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when completedBy is null")
        void shouldFailWhenCompletedByNull() {
            var count = createCountingCountAllCounted();

            var result = count.complete(null);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInitiatedBy.class);
        }

        @Test
        @DisplayName("should fail when completedBy is blank")
        void shouldFailWhenCompletedByBlank() {
            var count = createCountingCountAllCounted();

            var result = count.complete("  ");

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInitiatedBy.class);
        }

        @Test
        @DisplayName("should fail when no count items exist")
        void shouldFailWhenNoCountItems() {
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.COUNTING,
                    null, Instant.now(), new ArrayList<>()
            );

            var result = count.complete("user-2");

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.NoCountItems.class);
        }

        @Test
        @DisplayName("should fail when not all items have been counted")
        void shouldFailWhenIncompleteItems() {
            var items = new ArrayList<>(List.of(
                    CountItem.reconstitute(CountItemId.of("item-1"), ArticleId.of("article-1"),
                            Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM),
                            Quantity.reconstitute(new BigDecimal("8.0"), UnitOfMeasure.KILOGRAM)),
                    CountItem.reconstitute(CountItemId.of("item-2"), ArticleId.of("article-2"),
                            Quantity.reconstitute(new BigDecimal("5.0"), UnitOfMeasure.LITER),
                            null)
            ));
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.COUNTING,
                    null, Instant.now(), items
            );

            var result = count.complete("user-2");

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.IncompleteCountItems.class);
        }

        @Test
        @DisplayName("should fail when all items are uncounted")
        void shouldFailWhenAllItemsUncounted() {
            var count = createCountingCount();

            var result = count.complete("user-2");

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.IncompleteCountItems.class);
        }

        @Test
        @DisplayName("should fail when completedBy equals initiatedBy (Vier-Augen-Prinzip)")
        void shouldFailWhenSamePerson() {
            var count = createCountingCountAllCounted();

            var result = count.complete("user-1");

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.SamePersonViolation.class);
        }

        @Test
        @DisplayName("should not modify status when validation fails")
        void shouldNotModifyStatusOnFailure() {
            var count = createCountingCountAllCounted();

            count.complete("user-1"); // Same person → fails

            assertThat(count.status()).isEqualTo(InventoryCountStatus.COUNTING);
            assertThat(count.completedBy()).isNull();
        }

        private InventoryCount createCountingCountAllCounted() {
            var items = createCountedItems();
            return InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.COUNTING,
                    null, Instant.now(), items
            );
        }

        private List<CountItem> createCountedItems() {
            return new ArrayList<>(List.of(
                    CountItem.reconstitute(CountItemId.of("item-1"), ArticleId.of("article-1"),
                            Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM),
                            Quantity.reconstitute(new BigDecimal("8.0"), UnitOfMeasure.KILOGRAM))
            ));
        }
    }

    // ==================== cancel ====================

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

        @Test
        @DisplayName("should cancel from OPEN status")
        void shouldCancelFromOpen() {
            var count = createOpenCount();

            var result = count.cancel("Nicht mehr benötigt");

            assertThat(result.isSuccess()).isTrue();
            assertThat(count.status()).isEqualTo(InventoryCountStatus.CANCELLED);
            assertThat(count.cancellationReason()).isEqualTo("Nicht mehr benötigt");
        }

        @Test
        @DisplayName("should cancel from COUNTING status")
        void shouldCancelFromCounting() {
            var count = createCountingCount();

            var result = count.cancel("Falsche Artikel");

            assertThat(result.isSuccess()).isTrue();
            assertThat(count.status()).isEqualTo(InventoryCountStatus.CANCELLED);
            assertThat(count.cancellationReason()).isEqualTo("Falsche Artikel");
        }

        @Test
        @DisplayName("should fail when status is COMPLETED")
        void shouldFailWhenStatusIsCompleted() {
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.now(), "user-1", "user-2", InventoryCountStatus.COMPLETED,
                    null, Instant.now(), new ArrayList<>()
            );

            var result = count.cancel("Zu spät");

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when status is already CANCELLED")
        void shouldFailWhenAlreadyCancelled() {
            var count = InventoryCount.reconstitute(
                    InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                    LocalDate.now(), "user-1", null, InventoryCountStatus.CANCELLED,
                    "Alter Grund", Instant.now(), new ArrayList<>()
            );

            var result = count.cancel("Neuer Grund");

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatusTransition.class);
        }

        @Test
        @DisplayName("should fail when reason is null")
        void shouldFailWhenReasonNull() {
            var count = createOpenCount();

            var result = count.cancel(null);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.CancellationReasonRequired.class);
        }

        @Test
        @DisplayName("should fail when reason is blank")
        void shouldFailWhenReasonBlank() {
            var count = createOpenCount();

            var result = count.cancel("  ");

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.CancellationReasonRequired.class);
        }

        @Test
        @DisplayName("should not modify status when validation fails")
        void shouldNotModifyStatusOnFailure() {
            var count = createOpenCount();

            count.cancel(null);

            assertThat(count.status()).isEqualTo(InventoryCountStatus.OPEN);
            assertThat(count.cancellationReason()).isNull();
        }
    }

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

    private InventoryCount createOpenCount() {
        return InventoryCount.create(
                new InventoryCountDraft("location-1", LocalDate.now().toString(), "user-1")
        ).unsafeGetValue();
    }

    private InventoryCount createCountingCount() {
        var items = createCountItems();
        return InventoryCount.reconstitute(
                InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                LocalDate.now(), "user-1", null, InventoryCountStatus.COUNTING,
                null, Instant.now(), items
        );
    }

    private List<CountItem> createCountItems() {
        return new ArrayList<>(List.of(
                CountItem.reconstitute(CountItemId.of("item-1"), ArticleId.of("article-1"),
                        Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM), null)
        ));
    }
}
