package de.effigenix.application.production;

import de.effigenix.application.production.command.AddRecipeIngredientCommand;
import de.effigenix.domain.production.*;
import de.effigenix.shared.common.Result;
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.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

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.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
@DisplayName("AddRecipeIngredient Use Case")
class AddRecipeIngredientTest {

    @Mock private RecipeRepository recipeRepository;
    @Mock private AuthorizationPort authPort;
    @Mock private RecipeCycleChecker cycleChecker;
    @Mock private UnitOfWork unitOfWork;

    private AddRecipeIngredient addRecipeIngredient;
    private ActorId performedBy;

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

    private Recipe draftRecipe() {
        return Recipe.create(new RecipeDraft(
                "Bratwurst", 1, RecipeType.FINISHED_PRODUCT,
                null, 85, 14, "100", "KILOGRAM", "article-123"
        )).unsafeGetValue();
    }

    @Test
    @DisplayName("should_AddIngredient_When_NoSubRecipeId")
    void should_AddIngredient_When_NoSubRecipeId() {
        var recipe = draftRecipe();
        when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
        when(recipeRepository.findById(recipe.id())).thenReturn(Result.success(Optional.of(recipe)));
        when(recipeRepository.save(any())).thenReturn(Result.success(null));

        var cmd = new AddRecipeIngredientCommand(
                recipe.id().value(), 1, "article-123", "5.5", "KILOGRAM", null, false);

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

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().ingredients()).hasSize(1);
        verifyNoInteractions(cycleChecker);
    }

    @Test
    @DisplayName("should_AddIngredient_When_SubRecipeIdWithoutCycle")
    void should_AddIngredient_When_SubRecipeIdWithoutCycle() {
        var recipe = draftRecipe();
        when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
        when(recipeRepository.findById(recipe.id())).thenReturn(Result.success(Optional.of(recipe)));
        when(recipeRepository.save(any())).thenReturn(Result.success(null));
        when(cycleChecker.check(recipe.id().value(), "sub-recipe-1")).thenReturn(Result.success(null));

        var cmd = new AddRecipeIngredientCommand(
                recipe.id().value(), 1, "article-123", "5.5", "KILOGRAM", "sub-recipe-1", false);

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

        assertThat(result.isSuccess()).isTrue();
        verify(cycleChecker).check(recipe.id().value(), "sub-recipe-1");
    }

    @Test
    @DisplayName("should_FailWithUnauthorized_When_ActorLacksPermission")
    void should_FailWithUnauthorized_When_ActorLacksPermission() {
        when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(false);

        var cmd = new AddRecipeIngredientCommand(
                "recipe-1", 1, "article-123", "5.5", "KILOGRAM", "sub-recipe-1", false);

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

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.Unauthorized.class);
        verifyNoInteractions(cycleChecker);
        verify(recipeRepository, never()).save(any());
    }

    @Test
    @DisplayName("should_FailWithRecipeNotFound_When_RecipeDoesNotExist")
    void should_FailWithRecipeNotFound_When_RecipeDoesNotExist() {
        when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
        when(recipeRepository.findById(RecipeId.of("nonexistent"))).thenReturn(Result.success(Optional.empty()));

        var cmd = new AddRecipeIngredientCommand(
                "nonexistent", 1, "article-123", "5.5", "KILOGRAM", "sub-recipe-1", false);

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

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.RecipeNotFound.class);
        verifyNoInteractions(cycleChecker);
        verify(recipeRepository, never()).save(any());
    }

    @Test
    @DisplayName("should_FailWithCyclicDependency_When_CycleDetected")
    void should_FailWithCyclicDependency_When_CycleDetected() {
        var recipe = draftRecipe();
        when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
        when(recipeRepository.findById(recipe.id())).thenReturn(Result.success(Optional.of(recipe)));
        when(cycleChecker.check(recipe.id().value(), "sub-recipe-1"))
                .thenReturn(Result.failure(new RecipeError.CyclicDependencyDetected(
                        List.of(recipe.id().value(), "sub-recipe-1", recipe.id().value()))));

        var cmd = new AddRecipeIngredientCommand(
                recipe.id().value(), 1, "article-123", "5.5", "KILOGRAM", "sub-recipe-1", false);

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

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.CyclicDependencyDetected.class);
        verify(recipeRepository, never()).save(any());
    }
}
