package de.effigenix.application.masterdata;

import de.effigenix.application.masterdata.productcategory.command.CreateProductCategoryCommand;
import de.effigenix.application.masterdata.productcategory.command.UpdateProductCategoryCommand;
import de.effigenix.application.masterdata.productcategory.CreateProductCategory;
import de.effigenix.application.masterdata.productcategory.DeleteProductCategory;
import de.effigenix.application.masterdata.productcategory.ListProductCategories;
import de.effigenix.application.masterdata.productcategory.UpdateProductCategory;
import de.effigenix.domain.masterdata.article.Article;
import de.effigenix.domain.masterdata.article.ArticleRepository;
import de.effigenix.domain.masterdata.productcategory.*;
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 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.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("ProductCategory Use Cases")
class ProductCategoryUseCaseTest {

    @Mock private ProductCategoryRepository categoryRepository;
    @Mock private ArticleRepository articleRepository;
    @Mock private UnitOfWork unitOfWork;

    private ActorId performedBy;

    @BeforeEach
    void setUp() {
        lenient().when(unitOfWork.executeAtomically(any()))
                .thenAnswer(inv -> ((java.util.function.Supplier<?>) inv.getArgument(0)).get());
        performedBy = ActorId.of("admin-user");
    }

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

    private static ProductCategory existingCategory(String id, String name, String description) {
        return ProductCategory.reconstitute(
                ProductCategoryId.of(id),
                new CategoryName(name),
                description
        );
    }

    private static ProductCategory existingCategory(String id, String name) {
        return existingCategory(id, name, "Beschreibung für " + name);
    }

    // ==================== CreateProductCategory ====================

    @Nested
    @DisplayName("CreateProductCategory")
    class CreateProductCategoryTests {

        private CreateProductCategory useCase;

        @BeforeEach
        void setUp() {
            useCase = new CreateProductCategory(categoryRepository, unitOfWork);
        }

        @Test
        @DisplayName("should create category successfully")
        void shouldCreateCategorySuccessfully() {
            var cmd = new CreateProductCategoryCommand("Backwaren", "Alle Backwaren");
            when(categoryRepository.existsByName(any(CategoryName.class)))
                    .thenReturn(Result.success(false));
            when(categoryRepository.save(any(ProductCategory.class)))
                    .thenReturn(Result.success(null));

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

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().name().value()).isEqualTo("Backwaren");
            assertThat(result.unsafeGetValue().description()).isEqualTo("Alle Backwaren");
            verify(categoryRepository).existsByName(any(CategoryName.class));
            verify(categoryRepository).save(any(ProductCategory.class));
        }

        @Test
        @DisplayName("should create category with null description")
        void shouldCreateCategoryWithNullDescription() {
            var cmd = new CreateProductCategoryCommand("Backwaren", null);
            when(categoryRepository.existsByName(any(CategoryName.class)))
                    .thenReturn(Result.success(false));
            when(categoryRepository.save(any(ProductCategory.class)))
                    .thenReturn(Result.success(null));

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

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().description()).isNull();
        }

        @Test
        @DisplayName("should fail with ValidationFailure when name is blank")
        void shouldFailWhenNameIsBlank() {
            var cmd = new CreateProductCategoryCommand("  ", "Beschreibung");

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
            verifyNoInteractions(categoryRepository);
        }

        @Test
        @DisplayName("should fail with ValidationFailure when name is null")
        void shouldFailWhenNameIsNull() {
            var cmd = new CreateProductCategoryCommand(null, "Beschreibung");

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
            verifyNoInteractions(categoryRepository);
        }

        @Test
        @DisplayName("should fail with ValidationFailure when name exceeds 100 characters")
        void shouldFailWhenNameTooLong() {
            var longName = "A".repeat(101);
            var cmd = new CreateProductCategoryCommand(longName, "Beschreibung");

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
            verifyNoInteractions(categoryRepository);
        }

        @Test
        @DisplayName("should fail with CategoryNameAlreadyExists when name is taken")
        void shouldFailWhenNameAlreadyExists() {
            var cmd = new CreateProductCategoryCommand("Backwaren", "Beschreibung");
            when(categoryRepository.existsByName(any(CategoryName.class)))
                    .thenReturn(Result.success(true));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.CategoryNameAlreadyExists.class);
            verify(categoryRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when existsByName fails")
        void shouldFailWhenExistsByNameFails() {
            var cmd = new CreateProductCategoryCommand("Backwaren", "Beschreibung");
            when(categoryRepository.existsByName(any(CategoryName.class)))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

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

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var cmd = new CreateProductCategoryCommand("Backwaren", "Beschreibung");
            when(categoryRepository.existsByName(any(CategoryName.class)))
                    .thenReturn(Result.success(false));
            when(categoryRepository.save(any(ProductCategory.class)))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

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

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

    // ==================== UpdateProductCategory ====================

    @Nested
    @DisplayName("UpdateProductCategory")
    class UpdateProductCategoryTests {

        private UpdateProductCategory useCase;

        @BeforeEach
        void setUp() {
            useCase = new UpdateProductCategory(categoryRepository, unitOfWork);
        }

        @Test
        @DisplayName("should update name and description successfully")
        void shouldUpdateNameAndDescription() {
            var categoryId = "cat-1";
            var cmd = new UpdateProductCategoryCommand(categoryId, "Neuer Name", "Neue Beschreibung");
            when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
                    .thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
            when(categoryRepository.save(any(ProductCategory.class)))
                    .thenReturn(Result.success(null));

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

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().name().value()).isEqualTo("Neuer Name");
            assertThat(result.unsafeGetValue().description()).isEqualTo("Neue Beschreibung");
        }

        @Test
        @DisplayName("should update only name when description is null")
        void shouldUpdateOnlyName() {
            var categoryId = "cat-1";
            var cmd = new UpdateProductCategoryCommand(categoryId, "Neuer Name", null);
            var original = existingCategory(categoryId, "Alter Name", "Originalbeschreibung");
            when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
                    .thenReturn(Result.success(Optional.of(original)));
            when(categoryRepository.save(any(ProductCategory.class)))
                    .thenReturn(Result.success(null));

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

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().name().value()).isEqualTo("Neuer Name");
            assertThat(result.unsafeGetValue().description()).isEqualTo("Originalbeschreibung");
        }

        @Test
        @DisplayName("should update only description when name is null")
        void shouldUpdateOnlyDescription() {
            var categoryId = "cat-1";
            var cmd = new UpdateProductCategoryCommand(categoryId, null, "Neue Beschreibung");
            when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
                    .thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
            when(categoryRepository.save(any(ProductCategory.class)))
                    .thenReturn(Result.success(null));

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

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().name().value()).isEqualTo("Alter Name");
            assertThat(result.unsafeGetValue().description()).isEqualTo("Neue Beschreibung");
        }

        @Test
        @DisplayName("should fail with CategoryNotFound when category does not exist")
        void shouldFailWhenCategoryNotFound() {
            var cmd = new UpdateProductCategoryCommand("nonexistent", "Name", "Beschreibung");
            when(categoryRepository.findById(ProductCategoryId.of("nonexistent")))
                    .thenReturn(Result.success(Optional.empty()));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.CategoryNotFound.class);
            verify(categoryRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with ValidationFailure when new name is blank")
        void shouldFailWhenNewNameIsBlank() {
            var categoryId = "cat-1";
            var cmd = new UpdateProductCategoryCommand(categoryId, "  ", "Beschreibung");
            when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
                    .thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
            verify(categoryRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with ValidationFailure when new name exceeds 100 characters")
        void shouldFailWhenNewNameTooLong() {
            var categoryId = "cat-1";
            var longName = "B".repeat(101);
            var cmd = new UpdateProductCategoryCommand(categoryId, longName, null);
            when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
                    .thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
            verify(categoryRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var cmd = new UpdateProductCategoryCommand("cat-1", "Name", "Beschreibung");
            when(categoryRepository.findById(ProductCategoryId.of("cat-1")))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

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

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var categoryId = "cat-1";
            var cmd = new UpdateProductCategoryCommand(categoryId, "Neuer Name", null);
            when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
                    .thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
            when(categoryRepository.save(any(ProductCategory.class)))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

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

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

    // ==================== ListProductCategories ====================

    @Nested
    @DisplayName("ListProductCategories")
    class ListProductCategoriesTests {

        private ListProductCategories useCase;

        @BeforeEach
        void setUp() {
            useCase = new ListProductCategories(categoryRepository);
        }

        @Test
        @DisplayName("should return all categories")
        void shouldReturnAllCategories() {
            var categories = List.of(
                    existingCategory("cat-1", "Backwaren"),
                    existingCategory("cat-2", "Getränke")
            );
            when(categoryRepository.findAll()).thenReturn(Result.success(categories));

            var result = useCase.execute();

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(2);
        }

        @Test
        @DisplayName("should return empty list when no categories exist")
        void shouldReturnEmptyList() {
            when(categoryRepository.findAll()).thenReturn(Result.success(List.of()));

            var result = useCase.execute();

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

        @Test
        @DisplayName("should fail with RepositoryFailure when repository fails")
        void shouldFailWhenRepositoryFails() {
            when(categoryRepository.findAll())
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = useCase.execute();

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

    // ==================== DeleteProductCategory ====================

    @Nested
    @DisplayName("DeleteProductCategory")
    class DeleteProductCategoryTests {

        private DeleteProductCategory useCase;

        @BeforeEach
        void setUp() {
            useCase = new DeleteProductCategory(categoryRepository, articleRepository, unitOfWork);
        }

        @Test
        @DisplayName("should delete category successfully when not in use")
        void shouldDeleteCategorySuccessfully() {
            var categoryId = ProductCategoryId.of("cat-1");
            when(categoryRepository.findById(categoryId))
                    .thenReturn(Result.success(Optional.of(existingCategory("cat-1", "Backwaren"))));
            when(articleRepository.findByCategory(categoryId))
                    .thenReturn(Result.success(List.of()));
            when(categoryRepository.delete(any(ProductCategory.class)))
                    .thenReturn(Result.success(null));

            var result = useCase.execute(categoryId, performedBy);

            assertThat(result.isSuccess()).isTrue();
            verify(categoryRepository).delete(any(ProductCategory.class));
        }

        @Test
        @DisplayName("should fail with CategoryNotFound when category does not exist")
        void shouldFailWhenCategoryNotFound() {
            var categoryId = ProductCategoryId.of("nonexistent");
            when(categoryRepository.findById(categoryId))
                    .thenReturn(Result.success(Optional.empty()));

            var result = useCase.execute(categoryId, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.CategoryNotFound.class);
            verify(categoryRepository, never()).delete(any());
            verifyNoInteractions(articleRepository);
        }

        @Test
        @DisplayName("should fail with CategoryInUse when articles reference the category")
        void shouldFailWhenCategoryInUse() {
            var categoryId = ProductCategoryId.of("cat-1");
            when(categoryRepository.findById(categoryId))
                    .thenReturn(Result.success(Optional.of(existingCategory("cat-1", "Backwaren"))));
            // Return a non-empty list (using mock since Article is complex)
            var mockArticle = mock(Article.class);
            when(articleRepository.findByCategory(categoryId))
                    .thenReturn(Result.success(List.of(mockArticle)));

            var result = useCase.execute(categoryId, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.CategoryInUse.class);
            verify(categoryRepository, never()).delete(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var categoryId = ProductCategoryId.of("cat-1");
            when(categoryRepository.findById(categoryId))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = useCase.execute(categoryId, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when findByCategory fails")
        void shouldFailWhenFindByCategoryFails() {
            var categoryId = ProductCategoryId.of("cat-1");
            when(categoryRepository.findById(categoryId))
                    .thenReturn(Result.success(Optional.of(existingCategory("cat-1", "Backwaren"))));
            when(articleRepository.findByCategory(categoryId))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = useCase.execute(categoryId, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when delete fails")
        void shouldFailWhenDeleteFails() {
            var categoryId = ProductCategoryId.of("cat-1");
            when(categoryRepository.findById(categoryId))
                    .thenReturn(Result.success(Optional.of(existingCategory("cat-1", "Backwaren"))));
            when(articleRepository.findByCategory(categoryId))
                    .thenReturn(Result.success(List.of()));
            when(categoryRepository.delete(any(ProductCategory.class)))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

            var result = useCase.execute(categoryId, performedBy);

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