package de.effigenix.application.production;

import de.effigenix.application.production.command.CompleteBatchCommand;
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.event.DomainEvent;
import de.effigenix.shared.event.DomainEventPublisher;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort;
import de.effigenix.shared.persistence.UnitOfWork;
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.ArgumentCaptor;
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 java.util.Optional;
import java.util.function.Supplier;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("CompleteBatch Use Case")
class CompleteBatchTest {

    @Mock private BatchRepository batchRepository;
    @Mock private RecipeRepository recipeRepository;
    @Mock private AuthorizationPort authPort;
    @Mock private DomainEventPublisher domainEventPublisher;
    @Mock private UnitOfWork unitOfWork;

    private CompleteBatch completeBatch;
    private ActorId performedBy;

    @BeforeEach
    void setUp() {
        completeBatch = new CompleteBatch(batchRepository, recipeRepository, authPort, domainEventPublisher, unitOfWork);
        performedBy = ActorId.of("admin-user");
        lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
    }

    private Recipe activeRecipe(String recipeId) {
        return Recipe.reconstitute(
                RecipeId.of(recipeId),
                RecipeName.of("Test Recipe").unsafeGetValue(),
                1,
                RecipeType.FINISHED_PRODUCT,
                "desc",
                YieldPercentage.of(100).unsafeGetValue(),
                30,
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                "article-42",
                RecipeStatus.ACTIVE,
                List.of(),
                List.of(),
                OffsetDateTime.now(ZoneOffset.UTC),
                OffsetDateTime.now(ZoneOffset.UTC)
        );
    }

    private Batch inProductionBatchWithConsumption(String id) {
        var batch = Batch.reconstitute(
                BatchId.of(id),
                BatchNumber.generate(LocalDate.of(2026, 3, 1), 1),
                RecipeId.of("recipe-1"),
                BatchStatus.IN_PRODUCTION,
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                null, null, null,
                LocalDate.of(2026, 3, 1),
                LocalDate.of(2026, 6, 1),
                OffsetDateTime.now(ZoneOffset.UTC),
                OffsetDateTime.now(ZoneOffset.UTC),
                null,
                null, null,
                0L, List.of()
        );
        batch.recordConsumption(new ConsumptionDraft("input-1", "article-1", "5.0", "KILOGRAM"));
        return batch;
    }

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

    private CompleteBatchCommand validCommand(String batchId) {
        return new CompleteBatchCommand(batchId, "95", "KILOGRAM", "5", "KILOGRAM", "Test remarks");
    }

    @Test
    @DisplayName("should complete batch with actual quantity and waste")
    void should_CompleteBatch_When_Valid() {
        var batchId = BatchId.of("batch-1");
        var batch = inProductionBatchWithConsumption("batch-1");
        var recipe = activeRecipe("recipe-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
        when(batchRepository.save(any())).thenReturn(Result.success(null));
        when(recipeRepository.findById(RecipeId.of("recipe-1"))).thenReturn(Result.success(Optional.of(recipe)));

        var result = completeBatch.execute(validCommand("batch-1"), performedBy);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().status()).isEqualTo(BatchStatus.COMPLETED);
        assertThat(result.unsafeGetValue().actualQuantity().amount()).isEqualByComparingTo(new BigDecimal("95"));
        assertThat(result.unsafeGetValue().waste().amount()).isEqualByComparingTo(new BigDecimal("5"));
        assertThat(result.unsafeGetValue().remarks()).isEqualTo("Test remarks");
        verify(batchRepository).save(batch);
    }

    @Test
    @DisplayName("should publish BatchCompleted event with articleId from recipe")
    void should_PublishEvent_When_BatchCompleted() {
        var batchId = BatchId.of("batch-1");
        var batch = inProductionBatchWithConsumption("batch-1");
        var recipe = activeRecipe("recipe-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
        when(batchRepository.save(any())).thenReturn(Result.success(null));
        when(recipeRepository.findById(RecipeId.of("recipe-1"))).thenReturn(Result.success(Optional.of(recipe)));

        completeBatch.execute(validCommand("batch-1"), performedBy);

        var captor = ArgumentCaptor.forClass(DomainEvent.class);
        verify(domainEventPublisher).publish(captor.capture());
        var event = captor.getValue();
        assertThat(event).isInstanceOf(de.effigenix.domain.production.event.BatchCompleted.class);
        var batchCompleted = (de.effigenix.domain.production.event.BatchCompleted) event;
        assertThat(batchCompleted.articleId()).isEqualTo("article-42");
        assertThat(batchCompleted.batchId()).isEqualTo(batchId);
        assertThat(batchCompleted.actualQuantity().amount()).isEqualByComparingTo(new BigDecimal("95"));
        assertThat(batchCompleted.bestBeforeDate()).isEqualTo(LocalDate.of(2026, 6, 1));
    }

    @Test
    @DisplayName("should fail when batch not found")
    void should_Fail_When_BatchNotFound() {
        when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
        when(batchRepository.findById(any())).thenReturn(Result.success(Optional.empty()));

        var result = completeBatch.execute(validCommand("nonexistent"), performedBy);

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

    @Test
    @DisplayName("should fail when no consumptions recorded")
    void should_Fail_When_NoConsumptions() {
        var batchId = BatchId.of("batch-1");
        var batch = inProductionBatchWithoutConsumption("batch-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));

        var result = completeBatch.execute(validCommand("batch-1"), performedBy);

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

    @Test
    @DisplayName("should fail when actualQuantity is zero")
    void should_Fail_When_ActualQuantityZero() {
        var batchId = BatchId.of("batch-1");
        var batch = inProductionBatchWithConsumption("batch-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));

        var cmd = new CompleteBatchCommand("batch-1", "0", "KILOGRAM", "5", "KILOGRAM", null);
        var result = completeBatch.execute(cmd, performedBy);

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

    @Test
    @DisplayName("should fail when batch is PLANNED (not in production)")
    void should_Fail_When_BatchIsPlanned() {
        var batchId = BatchId.of("batch-1");
        var batch = Batch.reconstitute(
                batchId,
                BatchNumber.generate(LocalDate.of(2026, 3, 1), 1),
                RecipeId.of("recipe-1"),
                BatchStatus.PLANNED,
                Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
                null, null, null,
                LocalDate.of(2026, 3, 1),
                LocalDate.of(2026, 6, 1),
                OffsetDateTime.now(ZoneOffset.UTC),
                OffsetDateTime.now(ZoneOffset.UTC),
                null,
                null, null,
                0L, List.of()
        );
        when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));

        var result = completeBatch.execute(validCommand("batch-1"), performedBy);

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

    @Test
    @DisplayName("should fail when waste is negative")
    void should_Fail_When_NegativeWaste() {
        var batchId = BatchId.of("batch-1");
        var batch = inProductionBatchWithConsumption("batch-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));

        var cmd = new CompleteBatchCommand("batch-1", "95", "KILOGRAM", "-1", "KILOGRAM", null);
        var result = completeBatch.execute(cmd, performedBy);

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

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

        var result = completeBatch.execute(validCommand("batch-1"), performedBy);

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

    @Test
    @DisplayName("should fail with RepositoryFailure on repository error during find")
    void should_FailWithRepositoryFailure_When_FindFails() {
        when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
        when(batchRepository.findById(any()))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

        var result = completeBatch.execute(validCommand("batch-1"), performedBy);

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

    @Test
    @DisplayName("should fail with RepositoryFailure on repository error during save")
    void should_FailWithRepositoryFailure_When_SaveFails() {
        var batchId = BatchId.of("batch-1");
        var batch = inProductionBatchWithConsumption("batch-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
        when(batchRepository.save(any()))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("write error")));

        var result = completeBatch.execute(validCommand("batch-1"), performedBy);

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