package de.effigenix.application.inventory;

import de.effigenix.application.inventory.command.CancelInventoryCountCommand;
import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
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.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Optional;
import java.util.function.Supplier;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("CancelInventoryCount Use Case")
class CancelInventoryCountTest {

    @Mock private InventoryCountRepository inventoryCountRepository;
    @Mock private AuthorizationPort authPort;
    @Mock private UnitOfWork unitOfWork;

    private CancelInventoryCount cancelInventoryCount;
    private ActorId performedBy;

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

    private CancelInventoryCountCommand validCommand() {
        return new CancelInventoryCountCommand("count-1", "Nicht mehr benötigt");
    }

    private InventoryCount countWithStatus(InventoryCountStatus status) {
        return InventoryCount.reconstitute(
                InventoryCountId.of("count-1"), StorageLocationId.of("location-1"),
                LocalDate.now(), "user-1", null, status,
                null, Instant.now(), new ArrayList<>()
        );
    }

    @Test
    @DisplayName("should cancel OPEN inventory count")
    void should_Cancel_OpenCount() {
        when(authPort.can(performedBy, InventoryAction.INVENTORY_COUNT_WRITE)).thenReturn(true);
        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.success(Optional.of(countWithStatus(InventoryCountStatus.OPEN))));
        when(inventoryCountRepository.save(any())).thenReturn(Result.success(null));

        var result = cancelInventoryCount.execute(validCommand(), performedBy);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().status()).isEqualTo(InventoryCountStatus.CANCELLED);
        assertThat(result.unsafeGetValue().cancellationReason()).isEqualTo("Nicht mehr benötigt");
        verify(inventoryCountRepository).save(any(InventoryCount.class));
    }

    @Test
    @DisplayName("should cancel COUNTING inventory count")
    void should_Cancel_CountingCount() {
        when(authPort.can(performedBy, InventoryAction.INVENTORY_COUNT_WRITE)).thenReturn(true);
        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.success(Optional.of(countWithStatus(InventoryCountStatus.COUNTING))));
        when(inventoryCountRepository.save(any())).thenReturn(Result.success(null));

        var result = cancelInventoryCount.execute(validCommand(), performedBy);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().status()).isEqualTo(InventoryCountStatus.CANCELLED);
        assertThat(result.unsafeGetValue().cancellationReason()).isEqualTo("Nicht mehr benötigt");
        verify(inventoryCountRepository).save(any(InventoryCount.class));
    }

    @Test
    @DisplayName("should fail when status is COMPLETED")
    void should_Fail_When_Completed() {
        when(authPort.can(performedBy, InventoryAction.INVENTORY_COUNT_WRITE)).thenReturn(true);
        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.success(Optional.of(countWithStatus(InventoryCountStatus.COMPLETED))));

        var result = cancelInventoryCount.execute(validCommand(), performedBy);

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

    @Test
    @DisplayName("should fail when status is already CANCELLED")
    void should_Fail_When_AlreadyCancelled() {
        when(authPort.can(performedBy, InventoryAction.INVENTORY_COUNT_WRITE)).thenReturn(true);
        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.success(Optional.of(countWithStatus(InventoryCountStatus.CANCELLED))));

        var result = cancelInventoryCount.execute(validCommand(), performedBy);

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

    @Test
    @DisplayName("should fail when inventory count not found")
    void should_Fail_When_NotFound() {
        when(authPort.can(performedBy, InventoryAction.INVENTORY_COUNT_WRITE)).thenReturn(true);
        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.success(Optional.empty()));

        var result = cancelInventoryCount.execute(validCommand(), performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InventoryCountNotFound.class);
        verify(inventoryCountRepository, never()).save(any());
    }

    @Test
    @DisplayName("should fail when unauthorized")
    void should_Fail_When_Unauthorized() {
        when(authPort.can(performedBy, InventoryAction.INVENTORY_COUNT_WRITE)).thenReturn(false);

        var result = cancelInventoryCount.execute(validCommand(), performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.Unauthorized.class);
        verify(inventoryCountRepository, never()).save(any());
    }

    @Test
    @DisplayName("should fail when repository findById returns error")
    void should_Fail_When_RepositoryError() {
        when(authPort.can(performedBy, InventoryAction.INVENTORY_COUNT_WRITE)).thenReturn(true);
        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

        var result = cancelInventoryCount.execute(validCommand(), performedBy);

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

    @Test
    @DisplayName("should fail when reason is blank")
    void should_Fail_When_ReasonBlank() {
        when(authPort.can(performedBy, InventoryAction.INVENTORY_COUNT_WRITE)).thenReturn(true);
        when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
                .thenReturn(Result.success(Optional.of(countWithStatus(InventoryCountStatus.OPEN))));

        var result = cancelInventoryCount.execute(
                new CancelInventoryCountCommand("count-1", "  "), performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.CancellationReasonRequired.class);
        verify(inventoryCountRepository, never()).save(any());
    }
}
