package de.effigenix.application.inventory;

import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure;
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 java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

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

class CheckStockExpiryTest {

    private InMemoryStockRepository stockRepository;
    private CheckStockExpiry useCase;

    @BeforeEach
    void setUp() {
        stockRepository = new InMemoryStockRepository();
        useCase = new CheckStockExpiry(stockRepository);
    }

    @Nested
    @DisplayName("execute()")
    class Execute {

        @Test
        @DisplayName("should mark expired batches and expiring soon batches")
        void shouldMarkExpiredAndExpiringSoon() {
            var today = LocalDate.of(2026, 6, 15);

            // Stock with minimumShelfLife=30, one expired batch, one expiring soon batch
            var expiredBatch = StockBatch.reconstitute(
                    StockBatchId.generate(),
                    new BatchReference("BATCH-001", BatchType.PRODUCED),
                    Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM),
                    LocalDate.of(2026, 6, 10), // expired
                    StockBatchStatus.AVAILABLE,
                    Instant.now()
            );
            var expiringSoonBatch = StockBatch.reconstitute(
                    StockBatchId.generate(),
                    new BatchReference("BATCH-002", BatchType.PRODUCED),
                    Quantity.reconstitute(new BigDecimal("5"), UnitOfMeasure.KILOGRAM),
                    LocalDate.of(2026, 7, 1), // 16 days, within 30-day threshold
                    StockBatchStatus.AVAILABLE,
                    Instant.now()
            );
            var stock = Stock.reconstitute(
                    StockId.generate(), ArticleId.of("article-1"), StorageLocationId.of("location-1"),
                    null, new MinimumShelfLife(30),
                    new ArrayList<>(List.of(expiredBatch, expiringSoonBatch)), List.of()
            );
            stockRepository.addStock(stock);

            var result = useCase.execute(today);

            assertThat(result.isSuccess()).isTrue();
            var checkResult = result.unsafeGetValue();
            assertThat(checkResult.expiredCount()).isEqualTo(1);
            assertThat(checkResult.expiringSoonCount()).isEqualTo(1);

            // Verify saved stock has correct statuses
            var savedStock = stockRepository.savedStocks.getFirst();
            assertThat(savedStock.batches().get(0).status()).isEqualTo(StockBatchStatus.EXPIRED);
            assertThat(savedStock.batches().get(1).status()).isEqualTo(StockBatchStatus.EXPIRING_SOON);
        }

        @Test
        @DisplayName("should return zero counts when no relevant stocks")
        void shouldReturnZeroWhenNoRelevantStocks() {
            stockRepository.stocksToReturn = List.of();

            var result = useCase.execute(LocalDate.of(2026, 6, 15));

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().expiredCount()).isZero();
            assertThat(result.unsafeGetValue().expiringSoonCount()).isZero();
        }

        @Test
        @DisplayName("should process multiple stocks")
        void shouldProcessMultipleStocks() {
            var today = LocalDate.of(2026, 6, 15);

            var batch1 = StockBatch.reconstitute(
                    StockBatchId.generate(),
                    new BatchReference("BATCH-001", BatchType.PRODUCED),
                    Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM),
                    LocalDate.of(2026, 6, 10), StockBatchStatus.AVAILABLE, Instant.now()
            );
            var stock1 = Stock.reconstitute(
                    StockId.generate(), ArticleId.of("article-1"), StorageLocationId.of("location-1"),
                    null, null, new ArrayList<>(List.of(batch1)), List.of()
            );

            var batch2 = StockBatch.reconstitute(
                    StockBatchId.generate(),
                    new BatchReference("BATCH-002", BatchType.PRODUCED),
                    Quantity.reconstitute(new BigDecimal("5"), UnitOfMeasure.KILOGRAM),
                    LocalDate.of(2026, 6, 12), StockBatchStatus.AVAILABLE, Instant.now()
            );
            var stock2 = Stock.reconstitute(
                    StockId.generate(), ArticleId.of("article-2"), StorageLocationId.of("location-2"),
                    null, null, new ArrayList<>(List.of(batch2)), List.of()
            );

            stockRepository.addStock(stock1);
            stockRepository.addStock(stock2);

            var result = useCase.execute(today);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().expiredCount()).isEqualTo(2);
            assertThat(stockRepository.savedStocks).hasSize(2);
        }

        @Test
        @DisplayName("should only count expired when stock has no minimumShelfLife")
        void shouldOnlyCountExpiredWithoutMinimumShelfLife() {
            var today = LocalDate.of(2026, 6, 15);

            var expiredBatch = StockBatch.reconstitute(
                    StockBatchId.generate(),
                    new BatchReference("BATCH-001", BatchType.PRODUCED),
                    Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM),
                    LocalDate.of(2026, 6, 10), StockBatchStatus.AVAILABLE, Instant.now()
            );
            var soonBatch = StockBatch.reconstitute(
                    StockBatchId.generate(),
                    new BatchReference("BATCH-002", BatchType.PRODUCED),
                    Quantity.reconstitute(new BigDecimal("5"), UnitOfMeasure.KILOGRAM),
                    LocalDate.of(2026, 6, 20), StockBatchStatus.AVAILABLE, Instant.now()
            );
            // No minimumShelfLife → expiringSoon should not be marked
            var stock = Stock.reconstitute(
                    StockId.generate(), ArticleId.of("article-1"), StorageLocationId.of("location-1"),
                    null, null, new ArrayList<>(List.of(expiredBatch, soonBatch)), List.of()
            );
            stockRepository.addStock(stock);

            var result = useCase.execute(today);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().expiredCount()).isEqualTo(1);
            assertThat(result.unsafeGetValue().expiringSoonCount()).isZero();
            assertThat(stockRepository.savedStocks.getFirst().batches().get(1).status())
                    .isEqualTo(StockBatchStatus.AVAILABLE);
        }

        @Test
        @DisplayName("should mark expired before expiring soon to prevent incorrect classification")
        void shouldMarkExpiredBeforeExpiringSoon() {
            var today = LocalDate.of(2026, 6, 15);

            // Batch that is expired (expiryDate < today) but also within minimumShelfLife threshold
            // Must become EXPIRED, not EXPIRING_SOON
            var batch = StockBatch.reconstitute(
                    StockBatchId.generate(),
                    new BatchReference("BATCH-001", BatchType.PRODUCED),
                    Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM),
                    LocalDate.of(2026, 6, 10), // expired, but also within 30-day threshold
                    StockBatchStatus.AVAILABLE,
                    Instant.now()
            );
            var stock = Stock.reconstitute(
                    StockId.generate(), ArticleId.of("article-1"), StorageLocationId.of("location-1"),
                    null, new MinimumShelfLife(30), new ArrayList<>(List.of(batch)), List.of()
            );
            stockRepository.addStock(stock);

            var result = useCase.execute(today);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().expiredCount()).isEqualTo(1);
            assertThat(result.unsafeGetValue().expiringSoonCount()).isZero();
            assertThat(stockRepository.savedStocks.getFirst().batches().getFirst().status())
                    .isEqualTo(StockBatchStatus.EXPIRED);
        }

        @Test
        @DisplayName("should propagate repository failure on load")
        void shouldPropagateRepositoryFailureOnLoad() {
            stockRepository.failOnFind = true;

            var result = useCase.execute(LocalDate.of(2026, 6, 15));

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

        @Test
        @DisplayName("should continue and count failures when save fails")
        void shouldContinueOnSaveFailure() {
            var batch = StockBatch.reconstitute(
                    StockBatchId.generate(),
                    new BatchReference("BATCH-001", BatchType.PRODUCED),
                    Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM),
                    LocalDate.of(2026, 6, 10),
                    StockBatchStatus.AVAILABLE,
                    Instant.now()
            );
            var stock = Stock.reconstitute(
                    StockId.generate(), ArticleId.of("article-1"), StorageLocationId.of("location-1"),
                    null, null, new ArrayList<>(List.of(batch)), List.of()
            );
            stockRepository.addStock(stock);
            stockRepository.failOnSave = true;

            var result = useCase.execute(LocalDate.of(2026, 6, 15));

            assertThat(result.isSuccess()).isTrue();
            var checkResult = result.unsafeGetValue();
            assertThat(checkResult.expiredCount()).isEqualTo(1);
            assertThat(checkResult.failedCount()).isEqualTo(1);
            assertThat(stockRepository.savedStocks).isEmpty();
        }
    }

    // ==================== In-Memory Test Double ====================

    private static class InMemoryStockRepository implements StockRepository {

        List<Stock> stocksToReturn = new ArrayList<>();
        List<Stock> savedStocks = new ArrayList<>();
        boolean failOnFind = false;
        boolean failOnSave = false;

        void addStock(Stock stock) {
            stocksToReturn.add(stock);
        }

        @Override
        public Result<RepositoryError, List<Stock>> findAllWithExpiryRelevantBatches(LocalDate referenceDate) {
            if (failOnFind) {
                return Result.failure(new RepositoryError.DatabaseError("Test DB error"));
            }
            return Result.success(stocksToReturn);
        }

        @Override
        public Result<RepositoryError, Void> save(Stock stock) {
            if (failOnSave) {
                return Result.failure(new RepositoryError.DatabaseError("Test DB error"));
            }
            savedStocks.add(stock);
            return Result.success(null);
        }

        // Unused methods for this test
        @Override public Result<RepositoryError, List<Stock>> findAllBelowMinimumLevel() { return Result.success(List.of()); }
        @Override public Result<RepositoryError, Optional<Stock>> findById(StockId id) { return Result.success(Optional.empty()); }
        @Override public Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(Optional.empty()); }
        @Override public Result<RepositoryError, Boolean> existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(false); }
        @Override public Result<RepositoryError, List<Stock>> findAll() { return Result.success(List.of()); }
        @Override public Result<RepositoryError, List<Stock>> findAllByStorageLocationId(StorageLocationId storageLocationId) { return Result.success(List.of()); }
        @Override public Result<RepositoryError, List<Stock>> findAllByArticleId(ArticleId articleId) { return Result.success(List.of()); }
    }
}
