package de.effigenix.application.production;

import de.effigenix.application.production.command.RecordConsumptionCommand;
import de.effigenix.domain.production.*;
import de.effigenix.domain.production.event.ConsumptionRecorded;
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.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("RecordConsumption Use Case")
class RecordConsumptionTest {

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

    private RecordConsumption recordConsumption;
    private ActorId performedBy;

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

    private Batch inProductionBatch(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 Batch plannedBatch(String id) {
        return Batch.reconstitute(
                BatchId.of(id),
                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()
        );
    }

    private RecordConsumptionCommand validCommand(String batchId) {
        return new RecordConsumptionCommand(batchId, "input-batch-1", "article-1", "5.0", "KILOGRAM");
    }

    @Test
    @DisplayName("should record consumption when batch is IN_PRODUCTION")
    void should_RecordConsumption_When_InProduction() {
        var batchId = BatchId.of("batch-1");
        var batch = inProductionBatch("batch-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_WRITE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
        when(batchRepository.save(any())).thenReturn(Result.success(null));

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

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().inputBatchId().value()).isEqualTo("input-batch-1");
        assertThat(result.unsafeGetValue().articleId().value()).isEqualTo("article-1");
        verify(batchRepository).save(batch);
    }

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

        var result = recordConsumption.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 batch is not IN_PRODUCTION")
    void should_Fail_When_NotInProduction() {
        var batchId = BatchId.of("batch-1");
        var batch = plannedBatch("batch-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_WRITE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));

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

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

    @Test
    @DisplayName("should fail when duplicate inputBatchId")
    void should_Fail_When_DuplicateInputBatch() {
        var batchId = BatchId.of("batch-1");
        var batch = inProductionBatch("batch-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_WRITE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
        when(batchRepository.save(any())).thenReturn(Result.success(null));

        // First consumption succeeds
        recordConsumption.execute(validCommand("batch-1"), performedBy);

        // Second with same inputBatchId fails
        var result = recordConsumption.execute(validCommand("batch-1"), performedBy);

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

    @Test
    @DisplayName("should fail when consumption quantity is invalid")
    void should_Fail_When_InvalidQuantity() {
        var batchId = BatchId.of("batch-1");
        var batch = inProductionBatch("batch-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_WRITE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));

        var cmd = new RecordConsumptionCommand("batch-1", "input-1", "article-1", "0", "KILOGRAM");

        var result = recordConsumption.execute(cmd, performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidConsumptionQuantity.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_WRITE)).thenReturn(false);

        var result = recordConsumption.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_WRITE)).thenReturn(true);
        when(batchRepository.findById(any()))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

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

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

    @Test
    @DisplayName("should publish ConsumptionRecorded event after successful consumption")
    void should_PublishEvent_When_ConsumptionSucceeds() {
        var batchId = BatchId.of("batch-1");
        var batch = inProductionBatch("batch-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_WRITE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
        when(batchRepository.save(any())).thenReturn(Result.success(null));

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

        var captor = ArgumentCaptor.forClass(ConsumptionRecorded.class);
        verify(domainEventPublisher).publish(captor.capture());
        var event = captor.getValue();
        assertThat(event.batchId()).isEqualTo(batchId);
        assertThat(event.inputBatchId()).isEqualTo("input-batch-1");
        assertThat(event.articleId()).isEqualTo("article-1");
        assertThat(event.quantity().amount()).isEqualByComparingTo(new BigDecimal("5.0"));
        assertThat(event.quantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM);
        assertThat(event.occurredAt()).isNotNull();
    }

    @Test
    @DisplayName("should not publish event when consumption fails")
    void should_NotPublishEvent_When_ConsumptionFails() {
        var batchId = BatchId.of("batch-1");
        var batch = plannedBatch("batch-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_WRITE)).thenReturn(true);
        when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));

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

        verify(domainEventPublisher, never()).publish(any());
    }

    @Test
    @DisplayName("should fail with RepositoryFailure on repository error during save")
    void should_FailWithRepositoryFailure_When_SaveFails() {
        var batchId = BatchId.of("batch-1");
        var batch = inProductionBatch("batch-1");
        when(authPort.can(performedBy, ProductionAction.BATCH_WRITE)).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 = recordConsumption.execute(validCommand("batch-1"), performedBy);

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