package de.effigenix.application.inventory;

import de.effigenix.application.inventory.command.RecordCountItemCommand;
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("RecordCountItem Use Case")
class RecordCountItemTest {

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

    private RecordCountItem recordCountItem;
    private final ActorId actorId = ActorId.of("user-1");

    @BeforeEach
    void setUp() {
        recordCountItem = new RecordCountItem(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 createCountingCount() {
        return 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),
                        CountItem.reconstitute(CountItemId.of("item-2"), ArticleId.of("article-2"),
                                Quantity.reconstitute(new BigDecimal("5.0"), UnitOfMeasure.LITER), null)
                )
        );
    }

    @Test
    @DisplayName("should record actual quantity on count item")
    void shouldRecordActualQuantity() {
        stubUnitOfWork();
        var count = createCountingCount();
        var cmd = new RecordCountItemCommand("count-1", "item-1", "8.5", "KILOGRAM");

        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 = recordCountItem.execute(cmd, actorId);

        assertThat(result.isSuccess()).isTrue();
        var item = result.unsafeGetValue().countItems().stream()
                .filter(ci -> ci.id().equals(CountItemId.of("item-1")))
                .findFirst().orElseThrow();
        assertThat(item.actualQuantity().amount()).isEqualByComparingTo(new BigDecimal("8.5"));
        assertThat(item.isCounted()).isTrue();
    }

    @Test
    @DisplayName("should fail when not authorized")
    void shouldFailWhenNotAuthorized() {
        when(authPort.can(any(ActorId.class), any())).thenReturn(false);
        var cmd = new RecordCountItemCommand("count-1", "item-1", "8.0", "KILOGRAM");

        var result = recordCountItem.execute(cmd, 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() {
        var cmd = new RecordCountItemCommand("count-1", "item-1", "8.0", "KILOGRAM");

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

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

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

    @Test
    @DisplayName("should fail when count item not found")
    void shouldFailWhenCountItemNotFound() {
        var count = createCountingCount();
        var cmd = new RecordCountItemCommand("count-1", "nonexistent-but-valid", "8.0", "KILOGRAM");

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

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

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

    @Test
    @DisplayName("should fail when countItemId is blank")
    void shouldFailWhenCountItemIdBlank() {
        var cmd = new RecordCountItemCommand("count-1", "  ", "8.0", "KILOGRAM");

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

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

    @Test
    @DisplayName("should fail when countItemId is null")
    void shouldFailWhenCountItemIdNull() {
        var cmd = new RecordCountItemCommand("count-1", null, "8.0", "KILOGRAM");

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

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

    @Test
    @DisplayName("should fail when quantity amount is invalid")
    void shouldFailWhenQuantityAmountInvalid() {
        var cmd = new RecordCountItemCommand("count-1", "item-1", "not-a-number", "KILOGRAM");

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

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

    @Test
    @DisplayName("should fail when quantity unit is invalid")
    void shouldFailWhenUnitInvalid() {
        var cmd = new RecordCountItemCommand("count-1", "item-1", "8.0", "INVALID_UNIT");

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

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

    @Test
    @DisplayName("should fail when UOM does not match expected")
    void shouldFailWhenUomMismatch() {
        var count = createCountingCount();
        var cmd = new RecordCountItemCommand("count-1", "item-1", "8.0", "LITER");

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

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

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

    @Test
    @DisplayName("should fail when inventoryCountId is blank")
    void shouldFailWhenIdBlank() {
        var cmd = new RecordCountItemCommand("", "item-1", "8.0", "KILOGRAM");

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

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

    @Test
    @DisplayName("should fail when quantity amount is null")
    void shouldFailWhenQuantityAmountNull() {
        var cmd = new RecordCountItemCommand("count-1", "item-1", null, "KILOGRAM");

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

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

    @Test
    @DisplayName("should fail when quantity unit is null")
    void shouldFailWhenUnitNull() {
        var cmd = new RecordCountItemCommand("count-1", "item-1", "8.0", null);

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

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

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

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

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

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

    @Test
    @DisplayName("should fail when repository findById fails")
    void shouldFailWhenRepositoryFails() {
        var cmd = new RecordCountItemCommand("count-1", "item-1", "8.0", "KILOGRAM");

        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

        var result = recordCountItem.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 count = createCountingCount();
        var cmd = new RecordCountItemCommand("count-1", "item-1", "8.0", "KILOGRAM");

        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 = recordCountItem.execute(cmd, actorId);

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

    @Test
    @DisplayName("should allow zero actual quantity")
    void shouldAllowZeroActualQuantity() {
        stubUnitOfWork();
        var count = createCountingCount();
        var cmd = new RecordCountItemCommand("count-1", "item-1", "0", "KILOGRAM");

        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 = recordCountItem.execute(cmd, actorId);

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

    @Test
    @DisplayName("should fail when negative actual quantity")
    void shouldFailWhenNegativeQuantity() {
        var count = createCountingCount();
        var cmd = new RecordCountItemCommand("count-1", "item-1", "-5", "KILOGRAM");

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

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

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