package 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 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 java.util.Optional;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("BatchTraceabilityService")
class BatchTraceabilityServiceTest {

    @Mock private BatchRepository batchRepository;

    private BatchTraceabilityService service;

    @BeforeEach
    void setUp() {
        service = new BatchTraceabilityService(batchRepository);
    }

    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()
        );
    }

    @Nested
    @DisplayName("traceForward")
    class TraceForward {

        @Test
        @DisplayName("should fail with BatchNotFound when start batch does not exist")
        void should_FailWithBatchNotFound_When_StartBatchDoesNotExist() {
            var startId = BatchId.of("nonexistent");
            when(batchRepository.findById(startId)).thenReturn(Result.success(Optional.empty()));

            var result = service.traceForward(startId);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when findById returns error")
        void should_FailWithRepositoryFailure_When_FindByIdReturnsError() {
            var startId = BatchId.of("start");
            when(batchRepository.findById(startId))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = service.traceForward(startId);

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

        @Test
        @DisplayName("should delegate to repository traceForward when start batch exists")
        void should_DelegateToRepository_When_StartBatchExists() {
            var startId = BatchId.of("start");
            when(batchRepository.findById(startId)).thenReturn(Result.success(Optional.of(sampleBatch("start"))));

            var traced = List.of(
                    new TracedBatch(sampleBatch("child-1"), 1),
                    new TracedBatch(sampleBatch("child-2"), 1)
            );
            when(batchRepository.traceForward(startId, BatchTraceabilityService.DEFAULT_MAX_DEPTH))
                    .thenReturn(Result.success(traced));

            var result = service.traceForward(startId);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(2);
            verify(batchRepository).traceForward(startId, BatchTraceabilityService.DEFAULT_MAX_DEPTH);
        }

        @Test
        @DisplayName("should pass custom maxDepth to repository")
        void should_PassCustomMaxDepth_ToRepository() {
            var startId = BatchId.of("start");
            when(batchRepository.findById(startId)).thenReturn(Result.success(Optional.of(sampleBatch("start"))));
            when(batchRepository.traceForward(startId, 5)).thenReturn(Result.success(List.of()));

            var result = service.traceForward(startId, 5);

            assertThat(result.isSuccess()).isTrue();
            verify(batchRepository).traceForward(startId, 5);
        }

        @Test
        @DisplayName("should return empty list when maxDepth is 0")
        void should_ReturnEmptyList_When_MaxDepthIsZero() {
            var startId = BatchId.of("start");
            when(batchRepository.findById(startId)).thenReturn(Result.success(Optional.of(sampleBatch("start"))));

            var result = service.traceForward(startId, 0);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when traceForward returns error")
        void should_FailWithRepositoryFailure_When_TraceForwardReturnsError() {
            var startId = BatchId.of("start");
            when(batchRepository.findById(startId)).thenReturn(Result.success(Optional.of(sampleBatch("start"))));
            when(batchRepository.traceForward(startId, BatchTraceabilityService.DEFAULT_MAX_DEPTH))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));

            var result = service.traceForward(startId);

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

        @Test
        @DisplayName("should return empty list when no downstream batches exist")
        void should_ReturnEmptyList_When_NoDownstreamBatches() {
            var startId = BatchId.of("start-batch");
            when(batchRepository.findById(startId)).thenReturn(Result.success(Optional.of(sampleBatch("start-batch"))));
            when(batchRepository.traceForward(startId, BatchTraceabilityService.DEFAULT_MAX_DEPTH))
                    .thenReturn(Result.success(List.of()));

            var result = service.traceForward(startId);

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

    @Nested
    @DisplayName("traceBackward")
    class TraceBackward {

        @Test
        @DisplayName("should fail with BatchNotFound when start batch does not exist")
        void should_FailWithBatchNotFound_When_StartBatchDoesNotExist() {
            var startId = BatchId.of("nonexistent");
            when(batchRepository.findById(startId)).thenReturn(Result.success(Optional.empty()));

            var result = service.traceBackward(startId);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when findById returns error")
        void should_FailWithRepositoryFailure_When_FindByIdReturnsError() {
            var startId = BatchId.of("start");
            when(batchRepository.findById(startId))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = service.traceBackward(startId);

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

        @Test
        @DisplayName("should delegate to repository traceBackward when start batch exists")
        void should_DelegateToRepository_When_StartBatchExists() {
            var startId = BatchId.of("start");
            when(batchRepository.findById(startId)).thenReturn(Result.success(Optional.of(sampleBatch("start"))));

            var traced = List.of(
                    new TracedBatch(sampleBatch("parent-1"), 1),
                    new TracedBatch(sampleBatch("parent-2"), 1)
            );
            when(batchRepository.traceBackward(startId, BatchTraceabilityService.DEFAULT_MAX_DEPTH))
                    .thenReturn(Result.success(traced));

            var result = service.traceBackward(startId);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(2);
            verify(batchRepository).traceBackward(startId, BatchTraceabilityService.DEFAULT_MAX_DEPTH);
        }

        @Test
        @DisplayName("should pass custom maxDepth to repository")
        void should_PassCustomMaxDepth_ToRepository() {
            var startId = BatchId.of("start");
            when(batchRepository.findById(startId)).thenReturn(Result.success(Optional.of(sampleBatch("start"))));
            when(batchRepository.traceBackward(startId, 5)).thenReturn(Result.success(List.of()));

            var result = service.traceBackward(startId, 5);

            assertThat(result.isSuccess()).isTrue();
            verify(batchRepository).traceBackward(startId, 5);
        }

        @Test
        @DisplayName("should return empty list when maxDepth is 0")
        void should_ReturnEmptyList_When_MaxDepthIsZero() {
            var startId = BatchId.of("start");
            when(batchRepository.findById(startId)).thenReturn(Result.success(Optional.of(sampleBatch("start"))));

            var result = service.traceBackward(startId, 0);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when traceBackward returns error")
        void should_FailWithRepositoryFailure_When_TraceBackwardReturnsError() {
            var startId = BatchId.of("start");
            when(batchRepository.findById(startId)).thenReturn(Result.success(Optional.of(sampleBatch("start"))));
            when(batchRepository.traceBackward(startId, BatchTraceabilityService.DEFAULT_MAX_DEPTH))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));

            var result = service.traceBackward(startId);

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

        @Test
        @DisplayName("should return empty list when no upstream batches exist")
        void should_ReturnEmptyList_When_NoUpstreamBatches() {
            var startId = BatchId.of("start-batch");
            when(batchRepository.findById(startId)).thenReturn(Result.success(Optional.of(sampleBatch("start-batch"))));
            when(batchRepository.traceBackward(startId, BatchTraceabilityService.DEFAULT_MAX_DEPTH))
                    .thenReturn(Result.success(List.of()));

            var result = service.traceBackward(startId);

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