package de.effigenix.application.production;

import de.effigenix.application.production.command.TraceBatchBackwardCommand;
import de.effigenix.domain.production.*;
import de.effigenix.shared.common.Quantity;
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.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("TraceBatchBackward Use Case")
class TraceBatchBackwardTest {

    @Mock private BatchTraceabilityService traceabilityService;
    @Mock private AuthorizationPort authPort;

    private TraceBatchBackward traceBatchBackward;
    private ActorId performedBy;

    @BeforeEach
    void setUp() {
        traceBatchBackward = new TraceBatchBackward(traceabilityService, authPort);
        performedBy = ActorId.of("admin-user");
    }

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

        var result = traceBatchBackward.execute(new TraceBatchBackwardCommand("batch-1"), performedBy);

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

    @Test
    @DisplayName("should delegate to BatchTraceabilityService when authorized")
    void should_DelegateToService_When_Authorized() {
        when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);

        var tracedBatch = new TracedBatch(sampleBatch("parent-1"), 1);
        when(traceabilityService.traceBackward(BatchId.of("batch-1")))
                .thenReturn(Result.success(List.of(tracedBatch)));

        var result = traceBatchBackward.execute(new TraceBatchBackwardCommand("batch-1"), performedBy);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue()).hasSize(1);
        assertThat(result.unsafeGetValue().get(0).batch().id().value()).isEqualTo("parent-1");
        verify(traceabilityService).traceBackward(BatchId.of("batch-1"));
    }

    @Test
    @DisplayName("should return empty list when no upstream batches exist")
    void should_ReturnEmptyList_When_NoUpstream() {
        when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
        when(traceabilityService.traceBackward(BatchId.of("batch-1")))
                .thenReturn(Result.success(List.of()));

        var result = traceBatchBackward.execute(new TraceBatchBackwardCommand("batch-1"), performedBy);

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

    @Test
    @DisplayName("should propagate domain error from service")
    void should_PropagateDomainError_FromService() {
        when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
        when(traceabilityService.traceBackward(BatchId.of("nonexistent")))
                .thenReturn(Result.failure(new BatchError.BatchNotFound(BatchId.of("nonexistent"))));

        var result = traceBatchBackward.execute(new TraceBatchBackwardCommand("nonexistent"), performedBy);

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

    private Batch sampleBatch(String id) {
        return Batch.reconstitute(
                BatchId.of(id),
                BatchNumber.generate(LocalDate.of(2026, 3, 1), 1),
                RecipeId.of("recipe-1"),
                BatchStatus.COMPLETED,
                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()
        );
    }
}
