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 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 java.util.Optional;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("StartInventoryCount Use Case")
class StartInventoryCountTest {

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

    private StartInventoryCount startInventoryCount;
    private final ActorId actorId = ActorId.of("user-1");

    @BeforeEach
    void setUp() {
        startInventoryCount = new StartInventoryCount(inventoryCountRepository, unitOfWork, authPort);
        lenient().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();
        });
    }

    private InventoryCount createOpenCountWithItems() {
        var items = List.of(
                CountItem.reconstitute(CountItemId.of("item-1"), ArticleId.of("article-1"),
                        Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM), null)
        );
        return InventoryCount.reconstitute(
                InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                LocalDate.now(), "user-1", null, InventoryCountStatus.OPEN,
                null, Instant.now(), items
        );
    }

    @Test
    @DisplayName("should start counting for OPEN count with items")
    void shouldStartCounting() {
        stubUnitOfWork();
        var count = createOpenCountWithItems();

        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.success(Optional.of(count)));
        when(inventoryCountRepository.save(any(InventoryCount.class)))
                .thenReturn(Result.success(null));

        var result = startInventoryCount.execute("count-1", actorId);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().status()).isEqualTo(InventoryCountStatus.COUNTING);
        verify(inventoryCountRepository).save(any(InventoryCount.class));
    }

    @Test
    @DisplayName("should fail when not authorized")
    void shouldFailWhenNotAuthorized() {
        when(authPort.can(any(ActorId.class), any())).thenReturn(false);

        var result = startInventoryCount.execute("count-1", actorId);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.Unauthorized.class);
        verify(inventoryCountRepository, never()).findById(any());
    }

    @Test
    @DisplayName("should fail when inventory count not found")
    void shouldFailWhenNotFound() {
        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.success(Optional.empty()));

        var result = startInventoryCount.execute("count-1", actorId);

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

    @Test
    @DisplayName("should fail when count has no items")
    void shouldFailWhenNoItems() {
        var emptyCount = InventoryCount.reconstitute(
                InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                LocalDate.now(), "user-1", null, InventoryCountStatus.OPEN,
                null, Instant.now(), List.of()
        );

        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.success(Optional.of(emptyCount)));

        var result = startInventoryCount.execute("count-1", actorId);

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

    @Test
    @DisplayName("should fail when count is already COUNTING")
    void shouldFailWhenAlreadyCounting() {
        var countingCount = InventoryCount.reconstitute(
                InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                LocalDate.now(), "user-1", null, InventoryCountStatus.COUNTING,
                null, Instant.now(), List.of(
                        CountItem.reconstitute(CountItemId.of("item-1"), ArticleId.of("article-1"),
                                Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM), null)
                )
        );

        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.success(Optional.of(countingCount)));

        var result = startInventoryCount.execute("count-1", actorId);

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

    @Test
    @DisplayName("should fail when inventoryCountId is blank")
    void shouldFailWhenIdBlank() {
        var result = startInventoryCount.execute("", actorId);

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

    @Test
    @DisplayName("should fail when inventoryCountId is null")
    void shouldFailWhenIdNull() {
        var result = startInventoryCount.execute(null, actorId);

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

    @Test
    @DisplayName("should fail when repository findById fails")
    void shouldFailWhenRepositoryFails() {
        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

        var result = startInventoryCount.execute("count-1", actorId);

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

    @Test
    @DisplayName("should fail when save fails")
    void shouldFailWhenSaveFails() {
        stubUnitOfWork();
        var count = createOpenCountWithItems();

        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.success(Optional.of(count)));
        when(inventoryCountRepository.save(any(InventoryCount.class)))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("save failed")));

        var result = startInventoryCount.execute("count-1", actorId);

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