package de.effigenix.application.production;

import de.effigenix.domain.production.*;
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.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("ListBatches Use Case")
class ListBatchesTest {

    @Mock private BatchRepository batchRepository;
    @Mock private RecipeRepository recipeRepository;
    @Mock private AuthorizationPort authPort;

    private ListBatches listBatches;
    private ActorId performedBy;

    private static final LocalDate PRODUCTION_DATE = LocalDate.of(2026, 3, 1);
    private static final RepositoryError DB_ERROR = new RepositoryError.DatabaseError("connection lost");

    @BeforeEach
    void setUp() {
        listBatches = new ListBatches(batchRepository, recipeRepository, authPort);
        performedBy = ActorId.of("admin-user");
    }

    private Batch sampleBatch(String id, BatchStatus status) {
        return Batch.reconstitute(
                BatchId.of(id),
                BatchNumber.generate(PRODUCTION_DATE, 1),
                RecipeId.of("recipe-1"),
                status,
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                null, null, null,
                PRODUCTION_DATE,
                LocalDate.of(2026, 6, 1),
                OffsetDateTime.now(ZoneOffset.UTC),
                OffsetDateTime.now(ZoneOffset.UTC),
                null,
                null, null,
                0L, List.of()
        );
    }

    private Recipe sampleRecipe(String id) {
        return Recipe.reconstitute(
                RecipeId.of(id), new RecipeName("Bratwurst"), 1, RecipeType.FINISHED_PRODUCT,
                "Beschreibung", new YieldPercentage(85), 14,
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                "article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
                OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
        );
    }

    @Nested
    @DisplayName("execute – alle Batches")
    class ExecuteAll {

        @Test
        @DisplayName("should return all batches")
        void should_ReturnAllBatches() {
            var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED), sampleBatch("b2", BatchStatus.PLANNED));
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(batchRepository.findAllSummary()).thenReturn(Result.success(batches));

            var result = listBatches.execute(performedBy);

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

        @Test
        @DisplayName("should return empty list when no batches exist")
        void should_ReturnEmptyList_When_NoBatchesExist() {
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(batchRepository.findAllSummary()).thenReturn(Result.success(List.of()));

            var result = listBatches.execute(performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when findAll fails")
        void should_FailWithRepositoryFailure_When_FindAllFails() {
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(batchRepository.findAllSummary()).thenReturn(Result.failure(DB_ERROR));

            var result = listBatches.execute(performedBy);

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

        @Test
        @DisplayName("should fail with Unauthorized when actor lacks permission")
        void should_FailWithUnauthorized() {
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false);

            var result = listBatches.execute(performedBy);

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

    @Nested
    @DisplayName("executeByStatus – nach Status filtern")
    class ExecuteByStatus {

        @Test
        @DisplayName("should return batches filtered by status")
        void should_ReturnBatches_FilteredByStatus() {
            var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED));
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(batchRepository.findByStatusSummary(BatchStatus.PLANNED)).thenReturn(Result.success(batches));

            var result = listBatches.executeByStatus(BatchStatus.PLANNED, performedBy);

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

        @Test
        @DisplayName("should return empty list when no batches match status")
        void should_ReturnEmptyList_When_NoBatchesMatchStatus() {
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(batchRepository.findByStatusSummary(BatchStatus.PLANNED)).thenReturn(Result.success(List.of()));

            var result = listBatches.executeByStatus(BatchStatus.PLANNED, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when findByStatus fails")
        void should_FailWithRepositoryFailure_When_FindByStatusFails() {
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(batchRepository.findByStatusSummary(BatchStatus.PLANNED)).thenReturn(Result.failure(DB_ERROR));

            var result = listBatches.executeByStatus(BatchStatus.PLANNED, performedBy);

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

        @Test
        @DisplayName("should fail with Unauthorized when actor lacks permission")
        void should_FailWithUnauthorized() {
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false);

            var result = listBatches.executeByStatus(BatchStatus.PLANNED, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class);
            verify(batchRepository, never()).findByStatus(any());
        }
    }

    @Nested
    @DisplayName("executeByProductionDate – nach Datum filtern")
    class ExecuteByProductionDate {

        @Test
        @DisplayName("should return batches filtered by production date")
        void should_ReturnBatches_FilteredByProductionDate() {
            var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED));
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(batchRepository.findByProductionDateSummary(PRODUCTION_DATE)).thenReturn(Result.success(batches));

            var result = listBatches.executeByProductionDate(PRODUCTION_DATE, performedBy);

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

        @Test
        @DisplayName("should return empty list when no batches match date")
        void should_ReturnEmptyList_When_NoBatchesMatchDate() {
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(batchRepository.findByProductionDateSummary(PRODUCTION_DATE)).thenReturn(Result.success(List.of()));

            var result = listBatches.executeByProductionDate(PRODUCTION_DATE, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when findByProductionDate fails")
        void should_FailWithRepositoryFailure_When_FindByProductionDateFails() {
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(batchRepository.findByProductionDateSummary(PRODUCTION_DATE)).thenReturn(Result.failure(DB_ERROR));

            var result = listBatches.executeByProductionDate(PRODUCTION_DATE, performedBy);

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

        @Test
        @DisplayName("should fail with Unauthorized when actor lacks permission")
        void should_FailWithUnauthorized() {
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false);

            var result = listBatches.executeByProductionDate(PRODUCTION_DATE, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class);
            verify(batchRepository, never()).findByProductionDate(any());
        }
    }

    @Nested
    @DisplayName("executeByArticleId – über Recipe-Lookup filtern")
    class ExecuteByArticleId {

        @Test
        @DisplayName("should return batches filtered by articleId via recipe lookup")
        void should_ReturnBatches_FilteredByArticleId() {
            var recipe = sampleRecipe("recipe-1");
            var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED));
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(recipeRepository.findByArticleId("article-123")).thenReturn(Result.success(List.of(recipe)));
            when(batchRepository.findByRecipeIdsSummary(List.of(RecipeId.of("recipe-1")))).thenReturn(Result.success(batches));

            var result = listBatches.executeByArticleId("article-123", performedBy);

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

        @Test
        @DisplayName("should return empty list when no recipes match articleId")
        void should_ReturnEmptyList_When_NoRecipesMatchArticleId() {
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(recipeRepository.findByArticleId("unknown-article")).thenReturn(Result.success(List.of()));

            var result = listBatches.executeByArticleId("unknown-article", performedBy);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).isEmpty();
            verify(batchRepository, never()).findByRecipeIds(any());
        }

        @Test
        @DisplayName("should return empty list when recipes found but no batches match")
        void should_ReturnEmptyList_When_RecipesFoundButNoBatches() {
            var recipe = sampleRecipe("recipe-1");
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(recipeRepository.findByArticleId("article-123")).thenReturn(Result.success(List.of(recipe)));
            when(batchRepository.findByRecipeIdsSummary(List.of(RecipeId.of("recipe-1")))).thenReturn(Result.success(List.of()));

            var result = listBatches.executeByArticleId("article-123", performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when recipe repository fails")
        void should_FailWithRepositoryFailure_When_RecipeRepositoryFails() {
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(recipeRepository.findByArticleId("article-123")).thenReturn(Result.failure(DB_ERROR));

            var result = listBatches.executeByArticleId("article-123", performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class);
            verify(batchRepository, never()).findByRecipeIds(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when batch repository fails after recipe lookup")
        void should_FailWithRepositoryFailure_When_BatchRepositoryFailsAfterRecipeLookup() {
            var recipe = sampleRecipe("recipe-1");
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
            when(recipeRepository.findByArticleId("article-123")).thenReturn(Result.success(List.of(recipe)));
            when(batchRepository.findByRecipeIdsSummary(List.of(RecipeId.of("recipe-1")))).thenReturn(Result.failure(DB_ERROR));

            var result = listBatches.executeByArticleId("article-123", performedBy);

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

        @Test
        @DisplayName("should fail with Unauthorized when actor lacks permission")
        void should_FailWithUnauthorized() {
            when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false);

            var result = listBatches.executeByArticleId("article-123", performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class);
            verify(recipeRepository, never()).findByArticleId(any());
            verify(batchRepository, never()).findByRecipeIds(any());
        }
    }
}
