package de.effigenix.application.inventory;

import de.effigenix.application.inventory.command.ConfirmReservationCommand;
import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.article.ArticleId;
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.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.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("ConfirmReservation Use Case")
class ConfirmReservationTest {

    @Mock private StockRepository stockRepository;
    @Mock private StockMovementRepository stockMovementRepository;
    @Mock private UnitOfWork unitOfWork;

    private ConfirmReservation confirmReservation;

    @BeforeEach
    void setUp() {
        confirmReservation = new ConfirmReservation(stockRepository, stockMovementRepository, unitOfWork);
        lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv ->
                ((java.util.function.Supplier<?>) inv.getArgument(0)).get());
    }

    @Test
    @DisplayName("should confirm reservation, save stock and create stock movements")
    void shouldConfirmReservationSuccessfully() {
        var stock = createStockWithReservation("PRODUCTION_ORDER", "PO-001", "10");
        var reservationId = stock.reservations().getFirst().id().value();

        when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
        when(stockRepository.save(any())).thenReturn(Result.success(null));
        when(stockMovementRepository.save(any())).thenReturn(Result.success(null));

        var result = confirmReservation.execute(
                new ConfirmReservationCommand(stock.id().value(), reservationId, "admin"));

        assertThat(result.isSuccess()).isTrue();
        verify(stockRepository).save(stock);

        var movementCaptor = ArgumentCaptor.forClass(StockMovement.class);
        verify(stockMovementRepository).save(movementCaptor.capture());
        var movement = movementCaptor.getValue();
        assertThat(movement.movementType()).isEqualTo(MovementType.PRODUCTION_CONSUMPTION);
        assertThat(movement.direction()).isEqualTo(MovementDirection.OUT);
        assertThat(movement.quantity().amount()).isEqualByComparingTo(new BigDecimal("10"));
        assertThat(movement.performedBy()).isEqualTo("admin");
    }

    @Test
    @DisplayName("should derive SALE movement type from SALE_ORDER reference type")
    void shouldDeriveSaleMovementType() {
        var stock = createStockWithReservation("SALE_ORDER", "SO-001", "5");
        var reservationId = stock.reservations().getFirst().id().value();

        when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
        when(stockRepository.save(any())).thenReturn(Result.success(null));
        when(stockMovementRepository.save(any())).thenReturn(Result.success(null));

        var result = confirmReservation.execute(
                new ConfirmReservationCommand(stock.id().value(), reservationId, "admin"));

        assertThat(result.isSuccess()).isTrue();
        var movementCaptor = ArgumentCaptor.forClass(StockMovement.class);
        verify(stockMovementRepository).save(movementCaptor.capture());
        assertThat(movementCaptor.getValue().movementType()).isEqualTo(MovementType.SALE);
    }

    @Test
    @DisplayName("should create separate movements for multi-batch reservation")
    void shouldCreateSeparateMovementsForMultiBatch() {
        var batch1 = StockBatch.reconstitute(
                StockBatchId.generate(),
                new BatchReference("BATCH-001", BatchType.PRODUCED),
                Quantity.reconstitute(new BigDecimal("6"), UnitOfMeasure.KILOGRAM),
                LocalDate.of(2026, 6, 30), StockBatchStatus.AVAILABLE, Instant.now()
        );
        var batch2 = StockBatch.reconstitute(
                StockBatchId.generate(),
                new BatchReference("BATCH-002", BatchType.PRODUCED),
                Quantity.reconstitute(new BigDecimal("8"), UnitOfMeasure.KILOGRAM),
                LocalDate.of(2026, 12, 31), StockBatchStatus.AVAILABLE, Instant.now()
        );
        var stock = Stock.reconstitute(
                StockId.of("stock-1"),
                ArticleId.of("article-1"),
                StorageLocationId.of("location-1"),
                null, null, new ArrayList<>(List.of(batch1, batch2)), List.of()
        );
        stock.reserve(new ReservationDraft("PRODUCTION_ORDER", "PO-001", "10", "KILOGRAM", "NORMAL"));
        var reservationId = stock.reservations().getFirst().id().value();

        when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
        when(stockRepository.save(any())).thenReturn(Result.success(null));
        when(stockMovementRepository.save(any())).thenReturn(Result.success(null));

        var result = confirmReservation.execute(
                new ConfirmReservationCommand("stock-1", reservationId, "admin"));

        assertThat(result.isSuccess()).isTrue();
        verify(stockMovementRepository, times(2)).save(any());
    }

    @Test
    @DisplayName("should fail with StockNotFound when stock does not exist")
    void shouldFailWhenStockNotFound() {
        when(stockRepository.findById(any())).thenReturn(Result.success(Optional.empty()));

        var result = confirmReservation.execute(
                new ConfirmReservationCommand("stock-1", "res-1", "admin"));

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(StockError.StockNotFound.class);
        verify(stockRepository, never()).save(any());
        verify(stockMovementRepository, never()).save(any());
    }

    @Test
    @DisplayName("should fail with ReservationNotFound when reservation does not exist")
    void shouldFailWhenReservationNotFound() {
        var stock = createStockWithBatch("20");
        when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));

        var result = confirmReservation.execute(
                new ConfirmReservationCommand(stock.id().value(), "nonexistent", "admin"));

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(StockError.ReservationNotFound.class);
        verify(stockRepository, never()).save(any());
        verify(stockMovementRepository, never()).save(any());
    }

    @Test
    @DisplayName("should fail with RepositoryFailure when findById fails")
    void shouldFailWhenFindByIdFails() {
        when(stockRepository.findById(any()))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

        var result = confirmReservation.execute(
                new ConfirmReservationCommand("stock-1", "res-1", "admin"));

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class);
        verify(stockRepository, never()).save(any());
    }

    @Test
    @DisplayName("should fail with RepositoryFailure when save fails")
    void shouldFailWhenSaveFails() {
        var stock = createStockWithReservation("PRODUCTION_ORDER", "PO-001", "10");
        var reservationId = stock.reservations().getFirst().id().value();

        when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
        when(stockRepository.save(any()))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

        var result = confirmReservation.execute(
                new ConfirmReservationCommand(stock.id().value(), reservationId, "admin"));

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

    @Test
    @DisplayName("should fail with RepositoryFailure when stockMovementRepository save fails")
    void shouldFailWhenStockMovementSaveFails() {
        var stock = createStockWithReservation("PRODUCTION_ORDER", "PO-001", "10");
        var reservationId = stock.reservations().getFirst().id().value();

        when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
        when(stockRepository.save(any())).thenReturn(Result.success(null));
        when(stockMovementRepository.save(any()))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

        var result = confirmReservation.execute(
                new ConfirmReservationCommand(stock.id().value(), reservationId, "admin"));

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

    @Test
    @DisplayName("should confirm only targeted reservation when stock has multiple")
    void shouldConfirmOnlyTargetedReservation() {
        var stock = createStockWithBatch("50");
        stock.reserve(new ReservationDraft("PRODUCTION_ORDER", "PO-001", "10", "KILOGRAM", "NORMAL"));
        stock.reserve(new ReservationDraft("SALE_ORDER", "SO-001", "5", "KILOGRAM", "URGENT"));
        assertThat(stock.reservations()).hasSize(2);
        var firstReservationId = stock.reservations().getFirst().id().value();

        when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
        when(stockRepository.save(any())).thenReturn(Result.success(null));
        when(stockMovementRepository.save(any())).thenReturn(Result.success(null));

        var result = confirmReservation.execute(
                new ConfirmReservationCommand(stock.id().value(), firstReservationId, "admin"));

        assertThat(result.isSuccess()).isTrue();
        assertThat(stock.reservations()).hasSize(1);
        assertThat(stock.reservations().getFirst().referenceId()).isEqualTo("SO-001");
    }

    // ==================== Helpers ====================

    private Stock createStockWithBatch(String amount) {
        var batch = StockBatch.reconstitute(
                StockBatchId.generate(),
                new BatchReference("BATCH-001", BatchType.PRODUCED),
                Quantity.reconstitute(new BigDecimal(amount), UnitOfMeasure.KILOGRAM),
                LocalDate.of(2026, 12, 31),
                StockBatchStatus.AVAILABLE,
                Instant.now()
        );
        return Stock.reconstitute(
                StockId.of("stock-1"),
                ArticleId.of("article-1"),
                StorageLocationId.of("location-1"),
                null, null, new ArrayList<>(List.of(batch)), List.of()
        );
    }

    private Stock createStockWithReservation(String referenceType, String referenceId, String amount) {
        var stock = createStockWithBatch("50");
        stock.reserve(new ReservationDraft(referenceType, referenceId, amount, "KILOGRAM", "NORMAL"));
        return stock;
    }
}
