package de.effigenix.application.inventory;

import de.effigenix.application.inventory.command.CreateInventoryCountCommand;
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 de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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.Instant;
import java.time.LocalDate;
import java.util.List;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("CreateInventoryCount Use Case")
class CreateInventoryCountTest {

    @Mock private InventoryCountRepository inventoryCountRepository;
    @Mock private StockRepository stockRepository;
    @Mock private UnitOfWork unitOfWork;
    @Mock private AuthorizationPort authPort;

    private CreateInventoryCount createInventoryCount;
    private final ActorId actorId = ActorId.of("user-1");

    @BeforeEach
    void setUp() {
        createInventoryCount = new CreateInventoryCount(inventoryCountRepository, stockRepository, unitOfWork, authPort);
        when(authPort.can(any(ActorId.class), any())).thenReturn(true);
    }

    private void stubUnitOfWork() {
        when(unitOfWork.executeAtomically(any())).thenAnswer(invocation -> {
            @SuppressWarnings("unchecked")
            var supplier = (java.util.function.Supplier<Result<?, ?>>) invocation.getArgument(0);
            return supplier.get();
        });
    }

    @Test
    @DisplayName("should create inventory count and auto-populate from stocks")
    void shouldCreateAndAutoPopulate() {
        stubUnitOfWork();
        var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), "user-1");

        when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(false));

        var stock = Stock.reconstitute(
                StockId.of("stock-1"), ArticleId.of("article-1"), StorageLocationId.of("location-1"),
                null, null,
                List.of(
                        StockBatch.reconstitute(
                                StockBatchId.of("batch-1"),
                                new BatchReference("B001", BatchType.PRODUCED),
                                Quantity.reconstitute(new BigDecimal("10.5"), UnitOfMeasure.KILOGRAM),
                                LocalDate.now().plusDays(30),
                                StockBatchStatus.AVAILABLE,
                                Instant.now()
                        ),
                        StockBatch.reconstitute(
                                StockBatchId.of("batch-2"),
                                new BatchReference("B002", BatchType.PRODUCED),
                                Quantity.reconstitute(new BigDecimal("5.0"), UnitOfMeasure.KILOGRAM),
                                LocalDate.now().plusDays(60),
                                StockBatchStatus.AVAILABLE,
                                Instant.now()
                        )
                ),
                List.of()
        );

        when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(List.of(stock)));

        when(inventoryCountRepository.save(any(InventoryCount.class)))
                .thenReturn(Result.success(null));

        var result = createInventoryCount.execute(cmd, actorId);

        assertThat(result.isSuccess()).isTrue();
        var count = result.unsafeGetValue();
        assertThat(count.storageLocationId().value()).isEqualTo("location-1");
        assertThat(count.status()).isEqualTo(InventoryCountStatus.OPEN);
        assertThat(count.countItems()).hasSize(1);
        assertThat(count.countItems().getFirst().articleId().value()).isEqualTo("article-1");
        assertThat(count.countItems().getFirst().expectedQuantity().amount())
                .isEqualByComparingTo(new BigDecimal("15.5"));
    }

    @Test
    @DisplayName("should create empty inventory count when no stocks exist")
    void shouldCreateEmptyWhenNoStocks() {
        stubUnitOfWork();
        var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), "user-1");

        when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(false));
        when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(List.of()));
        when(inventoryCountRepository.save(any(InventoryCount.class)))
                .thenReturn(Result.success(null));

        var result = createInventoryCount.execute(cmd, actorId);

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

    @Test
    @DisplayName("should fail when active count already exists")
    void shouldFailWhenActiveCountExists() {
        var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), "user-1");

        when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(true));

        var result = createInventoryCount.execute(cmd, actorId);

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

    @Test
    @DisplayName("should fail when storageLocationId is invalid")
    void shouldFailWhenStorageLocationIdInvalid() {
        var cmd = new CreateInventoryCountCommand("", LocalDate.now().toString(), "user-1");

        var result = createInventoryCount.execute(cmd, actorId);

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

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

        var result = createInventoryCount.execute(cmd, actorId);

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

    @Test
    @DisplayName("should fail when repository check fails")
    void shouldFailWhenRepositoryCheckFails() {
        var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), "user-1");

        when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

        var result = createInventoryCount.execute(cmd, actorId);

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

    @Test
    @DisplayName("should fail when stock repository fails")
    void shouldFailWhenStockRepositoryFails() {
        var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), "user-1");

        when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(false));
        when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("stock db down")));

        var result = createInventoryCount.execute(cmd, actorId);

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

    @Test
    @DisplayName("should fail when save fails")
    void shouldFailWhenSaveFails() {
        stubUnitOfWork();
        var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), "user-1");

        when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(false));
        when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(List.of()));
        when(inventoryCountRepository.save(any(InventoryCount.class)))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("save failed")));

        var result = createInventoryCount.execute(cmd, actorId);

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

    @Test
    @DisplayName("should skip stocks without batches")
    void shouldSkipStocksWithoutBatches() {
        stubUnitOfWork();
        var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), "user-1");

        when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(false));

        var stock = Stock.reconstitute(
                StockId.of("stock-1"), ArticleId.of("article-1"), StorageLocationId.of("location-1"),
                null, null, List.of(), List.of()
        );

        when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(List.of(stock)));
        when(inventoryCountRepository.save(any(InventoryCount.class)))
                .thenReturn(Result.success(null));

        var result = createInventoryCount.execute(cmd, actorId);

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

    @Test
    @DisplayName("should handle multiple stocks for same location")
    void shouldHandleMultipleStocks() {
        stubUnitOfWork();
        var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), "user-1");

        when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(false));

        var stock1 = Stock.reconstitute(
                StockId.of("stock-1"), ArticleId.of("article-1"), StorageLocationId.of("location-1"),
                null, null,
                List.of(StockBatch.reconstitute(
                        StockBatchId.of("batch-1"), new BatchReference("B001", BatchType.PRODUCED),
                        Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM),
                        LocalDate.now().plusDays(30), StockBatchStatus.AVAILABLE, Instant.now()
                )), List.of()
        );
        var stock2 = Stock.reconstitute(
                StockId.of("stock-2"), ArticleId.of("article-2"), StorageLocationId.of("location-1"),
                null, null,
                List.of(StockBatch.reconstitute(
                        StockBatchId.of("batch-2"), new BatchReference("B002", BatchType.PRODUCED),
                        Quantity.reconstitute(new BigDecimal("20.0"), UnitOfMeasure.LITER),
                        LocalDate.now().plusDays(60), StockBatchStatus.AVAILABLE, Instant.now()
                )), List.of()
        );

        when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(List.of(stock1, stock2)));
        when(inventoryCountRepository.save(any(InventoryCount.class)))
                .thenReturn(Result.success(null));

        var result = createInventoryCount.execute(cmd, actorId);

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

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

        var result = createInventoryCount.execute(cmd, actorId);

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

    @Test
    @DisplayName("should fail when countDate is unparseable")
    void shouldFailWhenCountDateUnparseable() {
        var cmd = new CreateInventoryCountCommand("location-1", "not-a-date", "user-1");

        var result = createInventoryCount.execute(cmd, actorId);

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