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.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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.assertThat;

@DisplayName("InventoryCountReconciliationService")
class InventoryCountReconciliationServiceTest {

    private InventoryCountReconciliationService service;

    @BeforeEach
    void setUp() {
        service = new InventoryCountReconciliationService();
    }

    @Test
    @DisplayName("should return empty list when no deviations")
    void shouldReturnEmptyWhenNoDeviations() {
        var count = createCompletedCount(List.of(
                countedItem("item-1", "article-1", "10.0", "10.0")
        ));
        var stocks = List.of(stockFor("article-1", "stock-1"));

        var result = service.reconcile(count, stocks, "user-2");

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

    @Test
    @DisplayName("should create IN adjustment for positive deviation (actual > expected)")
    void shouldCreateInAdjustmentForPositiveDeviation() {
        var count = createCompletedCount(List.of(
                countedItem("item-1", "article-1", "10.0", "15.0")
        ));
        var stocks = List.of(stockFor("article-1", "stock-1"));

        var result = service.reconcile(count, stocks, "user-2");

        assertThat(result.isSuccess()).isTrue();
        var drafts = result.unsafeGetValue();
        assertThat(drafts).hasSize(1);

        var draft = drafts.getFirst();
        assertThat(draft.direction()).isEqualTo("IN");
        assertThat(draft.quantityAmount()).isEqualTo("5.0");
        assertThat(draft.movementType()).isEqualTo("ADJUSTMENT");
        assertThat(draft.stockId()).isEqualTo("stock-1");
        assertThat(draft.articleId()).isEqualTo("article-1");
    }

    @Test
    @DisplayName("should create OUT adjustment for negative deviation (actual < expected)")
    void shouldCreateOutAdjustmentForNegativeDeviation() {
        var count = createCompletedCount(List.of(
                countedItem("item-1", "article-1", "10.0", "7.0")
        ));
        var stocks = List.of(stockFor("article-1", "stock-1"));

        var result = service.reconcile(count, stocks, "user-2");

        assertThat(result.isSuccess()).isTrue();
        var drafts = result.unsafeGetValue();
        assertThat(drafts).hasSize(1);

        var draft = drafts.getFirst();
        assertThat(draft.direction()).isEqualTo("OUT");
        assertThat(draft.quantityAmount()).isEqualTo("3.0");
    }

    @Test
    @DisplayName("should skip items with zero deviation")
    void shouldSkipZeroDeviation() {
        var count = createCompletedCount(List.of(
                countedItem("item-1", "article-1", "10.0", "10.0"),
                countedItem("item-2", "article-2", "5.0", "8.0")
        ));
        var stocks = List.of(
                stockFor("article-1", "stock-1"),
                stockFor("article-2", "stock-2")
        );

        var result = service.reconcile(count, stocks, "user-2");

        assertThat(result.isSuccess()).isTrue();
        var drafts = result.unsafeGetValue();
        assertThat(drafts).hasSize(1);
        assertThat(drafts.getFirst().articleId()).isEqualTo("article-2");
    }

    @Test
    @DisplayName("should create multiple adjustments for multiple deviating items")
    void shouldCreateMultipleAdjustments() {
        var count = createCompletedCount(List.of(
                countedItem("item-1", "article-1", "10.0", "12.0"),
                countedItem("item-2", "article-2", "5.0", "3.0"),
                countedItem("item-3", "article-3", "20.0", "20.0")
        ));
        var stocks = List.of(
                stockFor("article-1", "stock-1"),
                stockFor("article-2", "stock-2"),
                stockFor("article-3", "stock-3")
        );

        var result = service.reconcile(count, stocks, "user-2");

        assertThat(result.isSuccess()).isTrue();
        var drafts = result.unsafeGetValue();
        assertThat(drafts).hasSize(2);
        assertThat(drafts.get(0).direction()).isEqualTo("IN");
        assertThat(drafts.get(0).quantityAmount()).isEqualTo("2.0");
        assertThat(drafts.get(1).direction()).isEqualTo("OUT");
        assertThat(drafts.get(1).quantityAmount()).isEqualTo("2.0");
    }

    @Test
    @DisplayName("should fail when stock not found for deviating article")
    void shouldFailWhenStockNotFound() {
        var count = createCompletedCount(List.of(
                countedItem("item-1", "article-1", "10.0", "15.0")
        ));

        var result = service.reconcile(count, List.of(), "user-2");

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

    @Test
    @DisplayName("should fail when stock has no batches for deviating article")
    void shouldFailWhenStockHasNoBatches() {
        var count = createCompletedCount(List.of(
                countedItem("item-1", "article-1", "10.0", "15.0")
        ));
        var emptyStock = Stock.reconstitute(
                StockId.of("stock-1"), ArticleId.of("article-1"),
                StorageLocationId.of("location-1"), null, null,
                List.of(), List.of()
        );

        var result = service.reconcile(count, List.of(emptyStock), "user-2");

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

    @Test
    @DisplayName("should not fail for zero-deviation item even without stock")
    void shouldNotFailForZeroDeviationWithoutStock() {
        var count = createCompletedCount(List.of(
                countedItem("item-1", "article-1", "10.0", "10.0")
        ));

        var result = service.reconcile(count, List.of(), "user-2");

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

    @Test
    @DisplayName("should use correct batch data in draft")
    void shouldUseCorrectBatchData() {
        var count = createCompletedCount(List.of(
                countedItem("item-1", "article-1", "10.0", "12.0")
        ));
        var stocks = List.of(stockFor("article-1", "stock-1"));

        var result = service.reconcile(count, stocks, "user-2");

        assertThat(result.isSuccess()).isTrue();
        var draft = result.unsafeGetValue().getFirst();
        assertThat(draft.stockBatchId()).isEqualTo("batch-1");
        assertThat(draft.batchId()).isEqualTo("BATCH-001");
        assertThat(draft.batchType()).isEqualTo("PRODUCED");
    }

    @Test
    @DisplayName("should set correct reason containing inventory count ID")
    void shouldSetCorrectReason() {
        var count = createCompletedCount(List.of(
                countedItem("item-1", "article-1", "10.0", "12.0")
        ));
        var stocks = List.of(stockFor("article-1", "stock-1"));

        var result = service.reconcile(count, stocks, "user-2");

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().getFirst().reason()).contains("count-1");
    }

    @Test
    @DisplayName("should set performedBy from parameter")
    void shouldSetPerformedBy() {
        var count = createCompletedCount(List.of(
                countedItem("item-1", "article-1", "10.0", "12.0")
        ));
        var stocks = List.of(stockFor("article-1", "stock-1"));

        var result = service.reconcile(count, stocks, "completer-user");

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().getFirst().performedBy()).isEqualTo("completer-user");
    }

    @Test
    @DisplayName("should use correct unit of measure from count item")
    void shouldUseCorrectUom() {
        var items = new ArrayList<>(List.of(
                CountItem.reconstitute(CountItemId.of("item-1"), ArticleId.of("article-1"),
                        Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.LITER),
                        Quantity.reconstitute(new BigDecimal("12.0"), UnitOfMeasure.LITER))
        ));
        var count = InventoryCount.reconstitute(
                InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                LocalDate.now(), "user-1", "user-2", InventoryCountStatus.COMPLETED,
                null, Instant.now(), items
        );
        var stocks = List.of(stockFor("article-1", "stock-1"));

        var result = service.reconcile(count, stocks, "user-2");

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().getFirst().quantityUnit()).isEqualTo("LITER");
    }

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

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

    private CountItem countedItem(String itemId, String articleId, String expected, String actual) {
        return CountItem.reconstitute(
                CountItemId.of(itemId), ArticleId.of(articleId),
                Quantity.reconstitute(new BigDecimal(expected), UnitOfMeasure.KILOGRAM),
                Quantity.reconstitute(new BigDecimal(actual), UnitOfMeasure.KILOGRAM)
        );
    }

    private Stock stockFor(String articleId, String stockId) {
        var batch = StockBatch.reconstitute(
                StockBatchId.of("batch-1"),
                new BatchReference("BATCH-001", BatchType.PRODUCED),
                Quantity.reconstitute(new BigDecimal("100.0"), UnitOfMeasure.KILOGRAM),
                LocalDate.now().plusDays(30),
                StockBatchStatus.AVAILABLE,
                Instant.now()
        );
        return Stock.reconstitute(
                StockId.of(stockId), ArticleId.of(articleId),
                StorageLocationId.of("location-1"), null, null,
                List.of(batch), List.of()
        );
    }
}
