package de.effigenix.domain.masterdata;

import de.effigenix.domain.masterdata.productcategory.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class ProductCategoryTest {

    // ==================== Create ====================

    @Nested
    @DisplayName("create()")
    class Create {

        @Test
        @DisplayName("should create ProductCategory with valid data")
        void shouldCreateWithValidData() {
            var draft = new ProductCategoryDraft("Backwaren", "Alle Backwaren-Produkte");

            var result = ProductCategory.create(draft);

            assertThat(result.isSuccess()).isTrue();
            var category = result.unsafeGetValue();
            assertThat(category.id()).isNotNull();
            assertThat(category.name().value()).isEqualTo("Backwaren");
            assertThat(category.description()).isEqualTo("Alle Backwaren-Produkte");
        }

        @Test
        @DisplayName("should create ProductCategory without description")
        void shouldCreateWithoutDescription() {
            var draft = new ProductCategoryDraft("Backwaren", null);

            var result = ProductCategory.create(draft);

            assertThat(result.isSuccess()).isTrue();
            var category = result.unsafeGetValue();
            assertThat(category.name().value()).isEqualTo("Backwaren");
            assertThat(category.description()).isNull();
        }

        @Test
        @DisplayName("should fail when name is null")
        void shouldFailWhenNameNull() {
            var draft = new ProductCategoryDraft(null, "Beschreibung");

            var result = ProductCategory.create(draft);

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

        @Test
        @DisplayName("should fail when name is blank")
        void shouldFailWhenNameBlank() {
            var draft = new ProductCategoryDraft("   ", "Beschreibung");

            var result = ProductCategory.create(draft);

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

        @Test
        @DisplayName("should fail when name is empty")
        void shouldFailWhenNameEmpty() {
            var draft = new ProductCategoryDraft("", "Beschreibung");

            var result = ProductCategory.create(draft);

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

        @Test
        @DisplayName("should fail when name exceeds 100 characters")
        void shouldFailWhenNameTooLong() {
            var longName = "A".repeat(101);
            var draft = new ProductCategoryDraft(longName, null);

            var result = ProductCategory.create(draft);

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

        @Test
        @DisplayName("should accept name with exactly 100 characters")
        void shouldAcceptNameWith100Chars() {
            var name = "A".repeat(100);
            var draft = new ProductCategoryDraft(name, null);

            var result = ProductCategory.create(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().name().value()).isEqualTo(name);
        }

        @Test
        @DisplayName("should generate unique IDs for different categories")
        void shouldGenerateUniqueIds() {
            var cat1 = ProductCategory.create(new ProductCategoryDraft("Kategorie 1", null)).unsafeGetValue();
            var cat2 = ProductCategory.create(new ProductCategoryDraft("Kategorie 2", null)).unsafeGetValue();

            assertThat(cat1.id()).isNotEqualTo(cat2.id());
        }
    }

    // ==================== Update ====================

    @Nested
    @DisplayName("update()")
    class Update {

        @Test
        @DisplayName("should update name")
        void shouldUpdateName() {
            var category = createValidCategory();
            var updateDraft = new ProductCategoryUpdateDraft("Neuer Name", null);

            var result = category.update(updateDraft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(category.name().value()).isEqualTo("Neuer Name");
        }

        @Test
        @DisplayName("should update description")
        void shouldUpdateDescription() {
            var category = createValidCategory();
            var updateDraft = new ProductCategoryUpdateDraft(null, "Neue Beschreibung");

            var result = category.update(updateDraft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(category.description()).isEqualTo("Neue Beschreibung");
        }

        @Test
        @DisplayName("should update name and description together")
        void shouldUpdateNameAndDescription() {
            var category = createValidCategory();
            var updateDraft = new ProductCategoryUpdateDraft("Neuer Name", "Neue Beschreibung");

            var result = category.update(updateDraft);

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

        @Test
        @DisplayName("should not change name when null in draft")
        void shouldNotChangeNameWhenNull() {
            var category = createValidCategory();
            var originalName = category.name().value();
            var updateDraft = new ProductCategoryUpdateDraft(null, null);

            var result = category.update(updateDraft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(category.name().value()).isEqualTo(originalName);
        }

        @Test
        @DisplayName("should not change description when null in draft")
        void shouldNotChangeDescriptionWhenNull() {
            var category = createValidCategory();
            var originalDescription = category.description();
            var updateDraft = new ProductCategoryUpdateDraft(null, null);

            var result = category.update(updateDraft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(category.description()).isEqualTo(originalDescription);
        }

        @Test
        @DisplayName("should fail update when name is blank")
        void shouldFailUpdateWhenNameBlank() {
            var category = createValidCategory();
            var originalName = category.name().value();
            var updateDraft = new ProductCategoryUpdateDraft("   ", null);

            var result = category.update(updateDraft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
            assertThat(category.name().value()).isEqualTo(originalName);
        }

        @Test
        @DisplayName("should fail update when name exceeds 100 characters")
        void shouldFailUpdateWhenNameTooLong() {
            var category = createValidCategory();
            var originalName = category.name().value();
            var updateDraft = new ProductCategoryUpdateDraft("A".repeat(101), null);

            var result = category.update(updateDraft);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
            assertThat(category.name().value()).isEqualTo(originalName);
        }
    }

    // ==================== Equality ====================

    @Nested
    @DisplayName("equals / hashCode")
    class Equality {

        @Test
        @DisplayName("should be equal if same ID")
        void shouldBeEqualBySameId() {
            var id = ProductCategoryId.generate();
            var cat1 = ProductCategory.reconstitute(id, new CategoryName("Backwaren"), "Beschreibung A");
            var cat2 = ProductCategory.reconstitute(id, new CategoryName("Getranke"), "Beschreibung B");

            assertThat(cat1).isEqualTo(cat2);
            assertThat(cat1.hashCode()).isEqualTo(cat2.hashCode());
        }

        @Test
        @DisplayName("should not be equal if different ID")
        void shouldNotBeEqualByDifferentId() {
            var cat1 = createValidCategory();
            var cat2 = createValidCategory();

            assertThat(cat1).isNotEqualTo(cat2);
        }

        @Test
        @DisplayName("should be equal to itself")
        void shouldBeEqualToItself() {
            var category = createValidCategory();

            assertThat(category).isEqualTo(category);
        }

        @Test
        @DisplayName("should not be equal to null")
        void shouldNotBeEqualToNull() {
            var category = createValidCategory();

            assertThat(category).isNotEqualTo(null);
        }

        @Test
        @DisplayName("should not be equal to different type")
        void shouldNotBeEqualToDifferentType() {
            var category = createValidCategory();

            assertThat(category).isNotEqualTo("not a category");
        }
    }

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

    private ProductCategory createValidCategory() {
        var draft = new ProductCategoryDraft("Backwaren", "Alle Backwaren-Produkte");
        return ProductCategory.create(draft).unsafeGetValue();
    }
}
