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.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.Nested;
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.util.List;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("ListStockMovements Use Case")
class ListStockMovementsTest {

    @Mock private StockMovementRepository repository;
    @Mock private AuthorizationPort authPort;

    private ListStockMovements useCase;
    private StockMovement sampleMovement;
    private final ActorId actor = ActorId.of("user-1");

    @BeforeEach
    void setUp() {
        useCase = new ListStockMovements(repository, authPort);
        when(authPort.can(any(ActorId.class), any())).thenReturn(true);

        sampleMovement = StockMovement.reconstitute(
                StockMovementId.generate(),
                StockId.of("stock-1"),
                ArticleId.of("article-1"),
                StockBatchId.of("batch-1"),
                new BatchReference("CHARGE-001", BatchType.PRODUCED),
                MovementType.GOODS_RECEIPT,
                MovementDirection.IN,
                Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM),
                null, null, "user-1", Instant.now()
        );
    }

    @Nested
    @DisplayName("No filters")
    class NoFilter {

        @Test
        @DisplayName("should return all movements when no filter")
        void shouldReturnAll() {
            when(repository.findAll()).thenReturn(Result.success(List.of(sampleMovement)));

            var result = useCase.execute(null, null, null, null, null, null, actor);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(1);
            verify(repository).findAll();
        }

        @Test
        @DisplayName("should return empty list when no movements exist")
        void shouldReturnEmptyList() {
            when(repository.findAll()).thenReturn(Result.success(List.of()));

            var result = useCase.execute(null, null, null, null, null, null, actor);

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

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

            var result = useCase.execute(null, null, null, null, null, null, actor);

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

    @Nested
    @DisplayName("Filter by stockId")
    class StockIdFilter {

        @Test
        @DisplayName("should filter by stockId")
        void shouldFilterByStockId() {
            when(repository.findAllByStockId(StockId.of("stock-1")))
                    .thenReturn(Result.success(List.of(sampleMovement)));

            var result = useCase.execute("stock-1", null, null, null, null, null, actor);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(1);
            verify(repository).findAllByStockId(StockId.of("stock-1"));
            verify(repository, never()).findAll();
        }

        @Test
        @DisplayName("should fail with InvalidStockId when format invalid")
        void shouldFailWhenStockIdInvalid() {
            var result = useCase.execute("  ", null, null, null, null, null, actor);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidStockId.class);
        }
    }

    @Nested
    @DisplayName("Filter by articleId")
    class ArticleIdFilter {

        @Test
        @DisplayName("should filter by articleId")
        void shouldFilterByArticleId() {
            when(repository.findAllByArticleId(ArticleId.of("article-1")))
                    .thenReturn(Result.success(List.of(sampleMovement)));

            var result = useCase.execute(null, "article-1", null, null, null, null, actor);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(1);
            verify(repository).findAllByArticleId(ArticleId.of("article-1"));
        }

        @Test
        @DisplayName("should fail with InvalidArticleId when format invalid")
        void shouldFailWhenArticleIdInvalid() {
            var result = useCase.execute(null, "  ", null, null, null, null, actor);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidArticleId.class);
        }
    }

    @Nested
    @DisplayName("Filter by batchReference")
    class BatchReferenceFilter {

        @Test
        @DisplayName("should filter by batchReference")
        void shouldFilterByBatchReference() {
            when(repository.findAllByBatchReference("CHARGE-001"))
                    .thenReturn(Result.success(List.of(sampleMovement)));

            var result = useCase.execute(null, null, null, "CHARGE-001", null, null, actor);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(1);
            verify(repository).findAllByBatchReference("CHARGE-001");
        }

        @Test
        @DisplayName("should return empty list when no movements for batch")
        void shouldReturnEmptyForUnknownBatch() {
            when(repository.findAllByBatchReference("UNKNOWN"))
                    .thenReturn(Result.success(List.of()));

            var result = useCase.execute(null, null, null, "UNKNOWN", null, null, actor);

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

        @Test
        @DisplayName("should fail with InvalidBatchReference when blank")
        void shouldFailWhenBatchReferenceBlank() {
            var result = useCase.execute(null, null, null, "  ", null, null, actor);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidBatchReference.class);
        }

        @Test
        @DisplayName("should fail when repository fails for batchReference")
        void shouldFailWhenRepositoryFailsForBatch() {
            when(repository.findAllByBatchReference("CHARGE-001"))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = useCase.execute(null, null, null, "CHARGE-001", null, null, actor);

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

    @Nested
    @DisplayName("Filter by movementType")
    class MovementTypeFilter {

        @Test
        @DisplayName("should filter by movementType")
        void shouldFilterByMovementType() {
            when(repository.findAllByMovementType(MovementType.GOODS_RECEIPT))
                    .thenReturn(Result.success(List.of(sampleMovement)));

            var result = useCase.execute(null, null, "GOODS_RECEIPT", null, null, null, actor);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(1);
            verify(repository).findAllByMovementType(MovementType.GOODS_RECEIPT);
        }

        @Test
        @DisplayName("should fail with InvalidMovementType when type invalid")
        void shouldFailWhenMovementTypeInvalid() {
            var result = useCase.execute(null, null, "INVALID_TYPE", null, null, null, actor);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidMovementType.class);
        }
    }

    @Nested
    @DisplayName("Filter by date range")
    class DateRangeFilter {

        private final Instant from = Instant.parse("2026-01-01T00:00:00Z");
        private final Instant to = Instant.parse("2026-12-31T23:59:59Z");

        @Test
        @DisplayName("should filter by from and to")
        void shouldFilterByFromAndTo() {
            when(repository.findAllByPerformedAtBetween(from, to))
                    .thenReturn(Result.success(List.of(sampleMovement)));

            var result = useCase.execute(null, null, null, null, from, to, actor);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(1);
            verify(repository).findAllByPerformedAtBetween(from, to);
        }

        @Test
        @DisplayName("should filter with only from (open-ended)")
        void shouldFilterByFromOnly() {
            when(repository.findAllByPerformedAtAfter(from))
                    .thenReturn(Result.success(List.of(sampleMovement)));

            var result = useCase.execute(null, null, null, null, from, null, actor);

            assertThat(result.isSuccess()).isTrue();
            verify(repository).findAllByPerformedAtAfter(from);
        }

        @Test
        @DisplayName("should filter with only to (open-ended)")
        void shouldFilterByToOnly() {
            when(repository.findAllByPerformedAtBefore(to))
                    .thenReturn(Result.success(List.of(sampleMovement)));

            var result = useCase.execute(null, null, null, null, null, to, actor);

            assertThat(result.isSuccess()).isTrue();
            verify(repository).findAllByPerformedAtBefore(to);
        }

        @Test
        @DisplayName("should fail with InvalidDateRange when from is after to")
        void shouldFailWhenFromAfterTo() {
            var result = useCase.execute(null, null, null, null, to, from, actor);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidDateRange.class);
            verify(repository, never()).findAllByPerformedAtBetween(any(), any());
        }

        @Test
        @DisplayName("should succeed when from equals to (same instant)")
        void shouldSucceedWhenFromEqualsTo() {
            when(repository.findAllByPerformedAtBetween(from, from))
                    .thenReturn(Result.success(List.of()));

            var result = useCase.execute(null, null, null, null, from, from, actor);

            assertThat(result.isSuccess()).isTrue();
            verify(repository).findAllByPerformedAtBetween(from, from);
        }

        @Test
        @DisplayName("should fail when repository fails for date range")
        void shouldFailWhenRepositoryFailsForDateRange() {
            when(repository.findAllByPerformedAtBetween(from, to))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = useCase.execute(null, null, null, null, from, to, actor);

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

    @Nested
    @DisplayName("Filter priority")
    class FilterPriority {

        @Test
        @DisplayName("stockId takes priority over articleId, batchReference and movementType")
        void stockIdTakesPriority() {
            when(repository.findAllByStockId(StockId.of("stock-1")))
                    .thenReturn(Result.success(List.of()));

            var result = useCase.execute("stock-1", "article-1", "GOODS_RECEIPT", "CHARGE-001", null, null, actor);

            assertThat(result.isSuccess()).isTrue();
            verify(repository).findAllByStockId(StockId.of("stock-1"));
            verify(repository, never()).findAllByArticleId(any());
            verify(repository, never()).findAllByBatchReference(any());
            verify(repository, never()).findAllByMovementType(any());
        }

        @Test
        @DisplayName("articleId takes priority over batchReference and movementType")
        void articleIdTakesPriorityOverBatchAndMovementType() {
            when(repository.findAllByArticleId(ArticleId.of("article-1")))
                    .thenReturn(Result.success(List.of()));

            var result = useCase.execute(null, "article-1", "GOODS_RECEIPT", "CHARGE-001", null, null, actor);

            assertThat(result.isSuccess()).isTrue();
            verify(repository).findAllByArticleId(ArticleId.of("article-1"));
            verify(repository, never()).findAllByBatchReference(any());
            verify(repository, never()).findAllByMovementType(any());
        }

        @Test
        @DisplayName("batchReference takes priority over movementType")
        void batchReferenceTakesPriorityOverMovementType() {
            when(repository.findAllByBatchReference("CHARGE-001"))
                    .thenReturn(Result.success(List.of()));

            var result = useCase.execute(null, null, "GOODS_RECEIPT", "CHARGE-001", null, null, actor);

            assertThat(result.isSuccess()).isTrue();
            verify(repository).findAllByBatchReference("CHARGE-001");
            verify(repository, never()).findAllByMovementType(any());
        }

        @Test
        @DisplayName("movementType takes priority over from/to")
        void movementTypeTakesPriorityOverDateRange() {
            when(repository.findAllByMovementType(MovementType.GOODS_RECEIPT))
                    .thenReturn(Result.success(List.of()));

            Instant from = Instant.parse("2026-01-01T00:00:00Z");
            Instant to = Instant.parse("2026-12-31T23:59:59Z");
            var result = useCase.execute(null, null, "GOODS_RECEIPT", null, from, to, actor);

            assertThat(result.isSuccess()).isTrue();
            verify(repository).findAllByMovementType(MovementType.GOODS_RECEIPT);
            verify(repository, never()).findAllByPerformedAtBetween(any(), any());
        }

        @Test
        @DisplayName("stockId takes priority over from/to")
        void stockIdTakesPriorityOverDateRange() {
            when(repository.findAllByStockId(StockId.of("stock-1")))
                    .thenReturn(Result.success(List.of()));

            Instant from = Instant.parse("2026-01-01T00:00:00Z");
            Instant to = Instant.parse("2026-12-31T23:59:59Z");
            var result = useCase.execute("stock-1", null, null, null, from, to, actor);

            assertThat(result.isSuccess()).isTrue();
            verify(repository).findAllByStockId(StockId.of("stock-1"));
            verify(repository, never()).findAllByPerformedAtBetween(any(), any());
        }

        @Test
        @DisplayName("batchReference takes priority over from/to")
        void batchReferenceTakesPriorityOverDateRange() {
            when(repository.findAllByBatchReference("CHARGE-001"))
                    .thenReturn(Result.success(List.of()));

            Instant from = Instant.parse("2026-01-01T00:00:00Z");
            Instant to = Instant.parse("2026-12-31T23:59:59Z");
            var result = useCase.execute(null, null, null, "CHARGE-001", from, to, actor);

            assertThat(result.isSuccess()).isTrue();
            verify(repository).findAllByBatchReference("CHARGE-001");
            verify(repository, never()).findAllByPerformedAtBetween(any(), any());
        }
    }

    @Nested
    @DisplayName("Authorization")
    class Authorization {

        @Test
        @DisplayName("should fail with Unauthorized when actor lacks STOCK_MOVEMENT_READ")
        void shouldFailWhenUnauthorized() {
            when(authPort.can(actor, InventoryAction.STOCK_MOVEMENT_READ)).thenReturn(false);

            var result = useCase.execute(null, null, null, null, null, null, actor);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.Unauthorized.class);
            verify(repository, never()).findAll();
        }
    }
}
