package de.effigenix.application.inventory;

import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
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.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.*;

@ExtendWith(MockitoExtension.class)
@DisplayName("ListInventoryCounts Use Case")
class ListInventoryCountsTest {

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

    private ListInventoryCounts listInventoryCounts;
    private InventoryCount count1;
    private InventoryCount count2;
    private final ActorId actorId = ActorId.of("user-1");

    @BeforeEach
    void setUp() {
        listInventoryCounts = new ListInventoryCounts(inventoryCountRepository, authPort);
        when(authPort.can(any(ActorId.class), any())).thenReturn(true);

        count1 = InventoryCount.reconstitute(
                InventoryCountId.of("count-1"),
                StorageLocationId.of("location-1"),
                LocalDate.now(),
                "user-1",
                null,
                InventoryCountStatus.OPEN,
                null,
                Instant.now(),
                List.of()
        );

        count2 = InventoryCount.reconstitute(
                InventoryCountId.of("count-2"),
                StorageLocationId.of("location-2"),
                LocalDate.now(),
                "user-1",
                null,
                InventoryCountStatus.COMPLETED,
                null,
                Instant.now(),
                List.of()
        );
    }

    @Test
    @DisplayName("should return all counts when no filter provided")
    void shouldReturnAllCountsWhenNoFilter() {
        when(inventoryCountRepository.findAll()).thenReturn(Result.success(List.of(count1, count2)));

        var result = listInventoryCounts.execute(null, null, actorId);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue()).hasSize(2);
        verify(inventoryCountRepository).findAll();
    }

    @Test
    @DisplayName("should filter by storageLocationId")
    void shouldFilterByStorageLocationId() {
        when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(List.of(count1)));

        var result = listInventoryCounts.execute("location-1", null, actorId);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue()).hasSize(1);
        verify(inventoryCountRepository).findByStorageLocationId(StorageLocationId.of("location-1"));
        verify(inventoryCountRepository, never()).findAll();
    }

    @Test
    @DisplayName("should fail with RepositoryFailure when repository fails for findAll")
    void shouldFailWhenRepositoryFailsForFindAll() {
        when(inventoryCountRepository.findAll())
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

        var result = listInventoryCounts.execute(null, null, actorId);

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

    @Test
    @DisplayName("should fail with RepositoryFailure when repository fails for storageLocationId filter")
    void shouldFailWhenRepositoryFailsForFilter() {
        when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

        var result = listInventoryCounts.execute("location-1", null, actorId);

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

    @Test
    @DisplayName("should return empty list when no counts match")
    void shouldReturnEmptyListWhenNoCountsMatch() {
        when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("unknown")))
                .thenReturn(Result.success(List.of()));

        var result = listInventoryCounts.execute("unknown", null, actorId);

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

    @Test
    @DisplayName("should fail with InvalidStorageLocationId for blank storageLocationId")
    void shouldFailWhenBlankStorageLocationId() {
        var result = listInventoryCounts.execute("  ", null, actorId);

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

    @Test
    @DisplayName("should filter by status only")
    void shouldFilterByStatusOnly() {
        when(inventoryCountRepository.findByStatus(InventoryCountStatus.OPEN))
                .thenReturn(Result.success(List.of(count1)));

        var result = listInventoryCounts.execute(null, "OPEN", actorId);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue()).hasSize(1);
        verify(inventoryCountRepository).findByStatus(InventoryCountStatus.OPEN);
        verify(inventoryCountRepository, never()).findAll();
    }

    @Test
    @DisplayName("should filter by storageLocationId and status")
    void shouldFilterByStorageLocationIdAndStatus() {
        when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1")))
                .thenReturn(Result.success(List.of(count1, count2)));

        var result = listInventoryCounts.execute("location-1", "OPEN", actorId);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue()).hasSize(1);
        assertThat(result.unsafeGetValue().getFirst().status()).isEqualTo(InventoryCountStatus.OPEN);
    }

    @Test
    @DisplayName("should fail with InvalidStatus for invalid status string")
    void shouldFailWhenInvalidStatus() {
        var result = listInventoryCounts.execute(null, "INVALID", actorId);

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

    @Test
    @DisplayName("should fail when unauthorized")
    void shouldFailWhenUnauthorized() {
        reset(authPort);
        when(authPort.can(any(ActorId.class), any())).thenReturn(false);

        var result = listInventoryCounts.execute(null, null, actorId);

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