package de.effigenix.domain.masterdata;

import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.domain.masterdata.customer.*;
import de.effigenix.shared.common.Address;
import de.effigenix.shared.common.Money;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Set;

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

class CustomerTest {

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

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

        @Nested
        @DisplayName("B2C")
        class B2C {

            @Test
            @DisplayName("should create B2C customer with valid data")
            void shouldCreateWithValidData() {
                var draft = new CustomerDraft(
                        "Max Mustermann", CustomerType.B2C,
                        "+49 123 456", "max@example.com", "Max",
                        "Hauptstr.", "10", "12345", "Berlin", "DE",
                        null, null
                );

                var result = Customer.create(draft);

                assertThat(result.isSuccess()).isTrue();
                var customer = result.unsafeGetValue();
                assertThat(customer.id()).isNotNull();
                assertThat(customer.name().value()).isEqualTo("Max Mustermann");
                assertThat(customer.type()).isEqualTo(CustomerType.B2C);
                assertThat(customer.billingAddress().street()).isEqualTo("Hauptstr.");
                assertThat(customer.billingAddress().houseNumber()).isEqualTo("10");
                assertThat(customer.billingAddress().postalCode()).isEqualTo("12345");
                assertThat(customer.billingAddress().city()).isEqualTo("Berlin");
                assertThat(customer.billingAddress().country()).isEqualTo("DE");
                assertThat(customer.contactInfo().phone()).isEqualTo("+49 123 456");
                assertThat(customer.contactInfo().email()).isEqualTo("max@example.com");
                assertThat(customer.contactInfo().contactPerson()).isEqualTo("Max");
                assertThat(customer.paymentTerms()).isNull();
                assertThat(customer.deliveryAddresses()).isEmpty();
                assertThat(customer.frameContract()).isNull();
                assertThat(customer.preferences()).isEmpty();
                assertThat(customer.status()).isEqualTo(CustomerStatus.ACTIVE);
                assertThat(customer.createdAt()).isNotNull();
                assertThat(customer.updatedAt()).isNotNull();
            }
        }

        @Nested
        @DisplayName("B2B")
        class B2B {

            @Test
            @DisplayName("should create B2B customer with valid data")
            void shouldCreateWithValidData() {
                var draft = new CustomerDraft(
                        "Firma GmbH", CustomerType.B2B,
                        "+49 800 123", "info@firma.de", "Herr Schmidt",
                        "Industriestr.", "5a", "80331", "Muenchen", "DE",
                        30, "30 Tage netto"
                );

                var result = Customer.create(draft);

                assertThat(result.isSuccess()).isTrue();
                var customer = result.unsafeGetValue();
                assertThat(customer.name().value()).isEqualTo("Firma GmbH");
                assertThat(customer.type()).isEqualTo(CustomerType.B2B);
                assertThat(customer.paymentTerms()).isNotNull();
                assertThat(customer.paymentTerms().paymentDueDays()).isEqualTo(30);
                assertThat(customer.paymentTerms().description()).isEqualTo("30 Tage netto");
            }
        }

        @Nested
        @DisplayName("validation failures")
        class ValidationFailures {

            @Test
            @DisplayName("should fail when name is null")
            void shouldFailWhenNameNull() {
                var draft = new CustomerDraft(
                        null, CustomerType.B2C,
                        "+49 123", null, null,
                        "Str.", null, "12345", "Berlin", "DE",
                        null, null
                );

                var result = Customer.create(draft);

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

            @Test
            @DisplayName("should fail when name is blank")
            void shouldFailWhenNameBlank() {
                var draft = new CustomerDraft(
                        "   ", CustomerType.B2C,
                        "+49 123", null, null,
                        "Str.", null, "12345", "Berlin", "DE",
                        null, null
                );

                var result = Customer.create(draft);

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

            @Test
            @DisplayName("should fail when name exceeds 200 characters")
            void shouldFailWhenNameTooLong() {
                var draft = new CustomerDraft(
                        "A".repeat(201), CustomerType.B2C,
                        "+49 123", null, null,
                        "Str.", null, "12345", "Berlin", "DE",
                        null, null
                );

                var result = Customer.create(draft);

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

            @Test
            @DisplayName("should accept name with exactly 200 characters")
            void shouldAcceptNameWith200Chars() {
                var draft = new CustomerDraft(
                        "A".repeat(200), CustomerType.B2C,
                        "+49 123", null, null,
                        "Str.", null, "12345", "Berlin", "DE",
                        null, null
                );

                var result = Customer.create(draft);

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

            @Test
            @DisplayName("should fail when email format is invalid")
            void shouldFailWhenEmailInvalid() {
                var draft = new CustomerDraft(
                        "Test", CustomerType.B2C,
                        "+49 123", "not-an-email", null,
                        "Str.", null, "12345", "Berlin", "DE",
                        null, null
                );

                var result = Customer.create(draft);

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

            @Test
            @DisplayName("should fail when street is blank")
            void shouldFailWhenStreetBlank() {
                var draft = new CustomerDraft(
                        "Test", CustomerType.B2C,
                        "+49 123", null, null,
                        "", null, "12345", "Berlin", "DE",
                        null, null
                );

                var result = Customer.create(draft);

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

            @Test
            @DisplayName("should fail when city is blank")
            void shouldFailWhenCityBlank() {
                var draft = new CustomerDraft(
                        "Test", CustomerType.B2C,
                        "+49 123", null, null,
                        "Str.", null, "12345", "", "DE",
                        null, null
                );

                var result = Customer.create(draft);

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

            @Test
            @DisplayName("should fail when country is not ISO alpha-2")
            void shouldFailWhenCountryInvalid() {
                var draft = new CustomerDraft(
                        "Test", CustomerType.B2C,
                        "+49 123", null, null,
                        "Str.", null, "12345", "Berlin", "Deutschland",
                        null, null
                );

                var result = Customer.create(draft);

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

            @Test
            @DisplayName("should fail when postalCode is blank")
            void shouldFailWhenPostalCodeBlank() {
                var draft = new CustomerDraft(
                        "Test", CustomerType.B2C,
                        "+49 123", null, null,
                        "Str.", null, "", "Berlin", "DE",
                        null, null
                );

                var result = Customer.create(draft);

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

            @Test
            @DisplayName("should fail when paymentDueDays is negative")
            void shouldFailWhenPaymentDueDaysNegative() {
                var draft = new CustomerDraft(
                        "Test", CustomerType.B2C,
                        "+49 123", null, null,
                        "Str.", null, "12345", "Berlin", "DE",
                        -1, null
                );

                var result = Customer.create(draft);

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

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

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

        @Test
        @DisplayName("should update name")
        void shouldUpdateName() {
            var customer = createValidCustomer();
            var draft = new CustomerUpdateDraft(
                    "Neuer Name", null, null, null,
                    null, null, null, null, null,
                    null, null
            );

            var result = customer.update(draft);

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

        @Test
        @DisplayName("should update contactInfo")
        void shouldUpdateContactInfo() {
            var customer = createValidCustomer();
            var draft = new CustomerUpdateDraft(
                    null, "+49 999 000", "new@example.com", "Frau Mueller",
                    null, null, null, null, null,
                    null, null
            );

            var result = customer.update(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(customer.contactInfo().phone()).isEqualTo("+49 999 000");
            assertThat(customer.contactInfo().email()).isEqualTo("new@example.com");
            assertThat(customer.contactInfo().contactPerson()).isEqualTo("Frau Mueller");
        }

        @Test
        @DisplayName("should update billingAddress")
        void shouldUpdateBillingAddress() {
            var customer = createValidCustomer();
            var draft = new CustomerUpdateDraft(
                    null, null, null, null,
                    "Neue Str.", "20", "54321", "Hamburg", "DE",
                    null, null
            );

            var result = customer.update(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(customer.billingAddress().street()).isEqualTo("Neue Str.");
            assertThat(customer.billingAddress().city()).isEqualTo("Hamburg");
        }

        @Test
        @DisplayName("should update paymentTerms")
        void shouldUpdatePaymentTerms() {
            var customer = createValidCustomer();
            var draft = new CustomerUpdateDraft(
                    null, null, null, null,
                    null, null, null, null, null,
                    60, "60 Tage netto"
            );

            var result = customer.update(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(customer.paymentTerms()).isNotNull();
            assertThat(customer.paymentTerms().paymentDueDays()).isEqualTo(60);
        }

        @Test
        @DisplayName("should not change fields when all null in draft")
        void shouldNotChangeFieldsWhenAllNull() {
            var customer = createValidCustomer();
            var originalName = customer.name().value();
            var originalPhone = customer.contactInfo().phone();
            var draft = new CustomerUpdateDraft(
                    null, null, null, null,
                    null, null, null, null, null,
                    null, null
            );

            var result = customer.update(draft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(customer.name().value()).isEqualTo(originalName);
            assertThat(customer.contactInfo().phone()).isEqualTo(originalPhone);
        }

        @Test
        @DisplayName("should fail update when name is blank")
        void shouldFailWhenNameBlank() {
            var customer = createValidCustomer();
            var draft = new CustomerUpdateDraft(
                    "  ", null, null, null,
                    null, null, null, null, null,
                    null, null
            );

            var result = customer.update(draft);

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

        @Test
        @DisplayName("should fail update when address city is blank")
        void shouldFailWhenAddressCityBlank() {
            var customer = createValidCustomer();
            var draft = new CustomerUpdateDraft(
                    null, null, null, null,
                    "Str.", null, "12345", "", "DE",
                    null, null
            );

            var result = customer.update(draft);

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

        @Test
        @DisplayName("should update updatedAt timestamp")
        void shouldUpdateTimestamp() {
            var customer = createValidCustomer();
            var beforeUpdate = customer.updatedAt();

            var draft = new CustomerUpdateDraft(
                    "Anderer Name", null, null, null,
                    null, null, null, null, null,
                    null, null
            );
            customer.update(draft);

            assertThat(customer.updatedAt()).isAfterOrEqualTo(beforeUpdate);
        }
    }

    // ==================== Activate / Deactivate ====================

    @Nested
    @DisplayName("activate() / deactivate()")
    class ActivateDeactivate {

        @Test
        @DisplayName("should deactivate active customer")
        void shouldDeactivate() {
            var customer = createValidCustomer();
            assertThat(customer.status()).isEqualTo(CustomerStatus.ACTIVE);

            customer.deactivate();

            assertThat(customer.status()).isEqualTo(CustomerStatus.INACTIVE);
        }

        @Test
        @DisplayName("should activate inactive customer")
        void shouldActivate() {
            var customer = createValidCustomer();
            customer.deactivate();
            assertThat(customer.status()).isEqualTo(CustomerStatus.INACTIVE);

            customer.activate();

            assertThat(customer.status()).isEqualTo(CustomerStatus.ACTIVE);
        }

        @Test
        @DisplayName("deactivate should update updatedAt")
        void deactivateShouldUpdateTimestamp() {
            var customer = createValidCustomer();
            var before = customer.updatedAt();

            customer.deactivate();

            assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
        }

        @Test
        @DisplayName("activate should update updatedAt")
        void activateShouldUpdateTimestamp() {
            var customer = createValidCustomer();
            customer.deactivate();
            var before = customer.updatedAt();

            customer.activate();

            assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
        }
    }

    // ==================== Delivery Addresses ====================

    @Nested
    @DisplayName("addDeliveryAddress() / removeDeliveryAddress()")
    class DeliveryAddresses {

        @Test
        @DisplayName("should add delivery address")
        void shouldAddDeliveryAddress() {
            var customer = createValidCustomer();
            var address = Address.create("Lieferstr.", "1", "99999", "Leipzig", "DE").unsafeGetValue();
            var deliveryAddress = new DeliveryAddress("Lager Ost", address, "Herr Meier", "Rampe 3");

            customer.addDeliveryAddress(deliveryAddress);

            assertThat(customer.deliveryAddresses()).hasSize(1);
            assertThat(customer.deliveryAddresses().getFirst().label()).isEqualTo("Lager Ost");
            assertThat(customer.deliveryAddresses().getFirst().address().city()).isEqualTo("Leipzig");
            assertThat(customer.deliveryAddresses().getFirst().contactPerson()).isEqualTo("Herr Meier");
            assertThat(customer.deliveryAddresses().getFirst().deliveryNotes()).isEqualTo("Rampe 3");
        }

        @Test
        @DisplayName("should add multiple delivery addresses")
        void shouldAddMultipleDeliveryAddresses() {
            var customer = createValidCustomer();
            var address1 = Address.create("Str. 1", null, "11111", "Stadt A", "DE").unsafeGetValue();
            var address2 = Address.create("Str. 2", null, "22222", "Stadt B", "DE").unsafeGetValue();

            customer.addDeliveryAddress(new DeliveryAddress("Lager 1", address1, null, null));
            customer.addDeliveryAddress(new DeliveryAddress("Lager 2", address2, null, null));

            assertThat(customer.deliveryAddresses()).hasSize(2);
        }

        @Test
        @DisplayName("should remove delivery address by label")
        void shouldRemoveDeliveryAddressByLabel() {
            var customer = createValidCustomer();
            var address = Address.create("Lieferstr.", "1", "99999", "Leipzig", "DE").unsafeGetValue();
            customer.addDeliveryAddress(new DeliveryAddress("Lager Ost", address, null, null));
            customer.addDeliveryAddress(new DeliveryAddress("Lager West", address, null, null));

            customer.removeDeliveryAddress("Lager Ost");

            assertThat(customer.deliveryAddresses()).hasSize(1);
            assertThat(customer.deliveryAddresses().getFirst().label()).isEqualTo("Lager West");
        }

        @Test
        @DisplayName("should do nothing when removing non-existent label")
        void shouldDoNothingWhenRemovingNonExistent() {
            var customer = createValidCustomer();
            var address = Address.create("Lieferstr.", "1", "99999", "Leipzig", "DE").unsafeGetValue();
            customer.addDeliveryAddress(new DeliveryAddress("Lager Ost", address, null, null));

            customer.removeDeliveryAddress("Nicht vorhanden");

            assertThat(customer.deliveryAddresses()).hasSize(1);
        }

        @Test
        @DisplayName("addDeliveryAddress should update updatedAt")
        void addShouldUpdateTimestamp() {
            var customer = createValidCustomer();
            var before = customer.updatedAt();
            var address = Address.create("Str.", null, "12345", "City", "DE").unsafeGetValue();

            customer.addDeliveryAddress(new DeliveryAddress("L", address, null, null));

            assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
        }

        @Test
        @DisplayName("removeDeliveryAddress should update updatedAt")
        void removeShouldUpdateTimestamp() {
            var customer = createValidCustomer();
            var address = Address.create("Str.", null, "12345", "City", "DE").unsafeGetValue();
            customer.addDeliveryAddress(new DeliveryAddress("L", address, null, null));
            var before = customer.updatedAt();

            customer.removeDeliveryAddress("L");

            assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
        }

        @Test
        @DisplayName("deliveryAddresses should return unmodifiable list")
        void shouldReturnUnmodifiableList() {
            var customer = createValidCustomer();

            var list = customer.deliveryAddresses();

            org.junit.jupiter.api.Assertions.assertThrows(
                    UnsupportedOperationException.class,
                    () -> list.add(null)
            );
        }
    }

    // ==================== Frame Contract ====================

    @Nested
    @DisplayName("setFrameContract()")
    class SetFrameContract {

        @Test
        @DisplayName("should set frame contract for B2B customer")
        void shouldSetFrameContractForB2B() {
            var customer = createValidB2BCustomer();
            var contract = createValidFrameContract();

            var result = customer.setFrameContract(contract);

            assertThat(result.isSuccess()).isTrue();
            assertThat(customer.frameContract()).isNotNull();
            assertThat(customer.frameContract()).isEqualTo(contract);
        }

        @Test
        @DisplayName("should fail setting frame contract for B2C customer")
        void shouldFailForB2C() {
            var customer = createValidCustomer();
            var contract = createValidFrameContract();

            var result = customer.setFrameContract(contract);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.FrameContractNotAllowed.class);
            assertThat(customer.frameContract()).isNull();
        }

        @Test
        @DisplayName("should remove frame contract")
        void shouldRemoveFrameContract() {
            var customer = createValidB2BCustomer();
            var contract = createValidFrameContract();
            customer.setFrameContract(contract);

            customer.removeFrameContract();

            assertThat(customer.frameContract()).isNull();
        }

        @Test
        @DisplayName("hasActiveFrameContract should return true for active contract")
        void shouldReturnTrueForActiveContract() {
            var customer = createValidB2BCustomer();
            var lineItem = new ContractLineItem(
                    new ArticleId("ART-001"), Money.euro(new BigDecimal("10.00")),
                    new BigDecimal("100"), Unit.KG
            );
            var contract = FrameContract.create(
                    LocalDate.now().minusDays(1), LocalDate.now().plusDays(30),
                    DeliveryRhythm.WEEKLY, List.of(lineItem)
            ).unsafeGetValue();
            customer.setFrameContract(contract);

            assertThat(customer.hasActiveFrameContract()).isTrue();
        }

        @Test
        @DisplayName("hasActiveFrameContract should return false when no contract")
        void shouldReturnFalseWhenNoContract() {
            var customer = createValidB2BCustomer();

            assertThat(customer.hasActiveFrameContract()).isFalse();
        }

        @Test
        @DisplayName("getAgreedPrice should return price for existing article")
        void shouldReturnAgreedPrice() {
            var customer = createValidB2BCustomer();
            var articleId = new ArticleId("ART-001");
            var price = Money.euro(new BigDecimal("12.50"));
            var lineItem = new ContractLineItem(articleId, price, new BigDecimal("100"), Unit.KG);
            var contract = FrameContract.create(
                    LocalDate.now().minusDays(1), LocalDate.now().plusDays(30),
                    DeliveryRhythm.WEEKLY, List.of(lineItem)
            ).unsafeGetValue();
            customer.setFrameContract(contract);

            var result = customer.getAgreedPrice(articleId);

            assertThat(result).isPresent();
            assertThat(result.get()).isEqualTo(price);
        }

        @Test
        @DisplayName("getAgreedPrice should return empty for unknown article")
        void shouldReturnEmptyForUnknownArticle() {
            var customer = createValidB2BCustomer();
            var lineItem = new ContractLineItem(
                    new ArticleId("ART-001"), Money.euro(new BigDecimal("10.00")),
                    new BigDecimal("100"), Unit.KG
            );
            var contract = FrameContract.create(
                    LocalDate.now().minusDays(1), LocalDate.now().plusDays(30),
                    DeliveryRhythm.WEEKLY, List.of(lineItem)
            ).unsafeGetValue();
            customer.setFrameContract(contract);

            var result = customer.getAgreedPrice(new ArticleId("ART-999"));

            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("getAgreedPrice should return empty when no contract set")
        void shouldReturnEmptyWhenNoContract() {
            var customer = createValidB2BCustomer();

            var result = customer.getAgreedPrice(new ArticleId("ART-001"));

            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("setFrameContract should update updatedAt")
        void shouldUpdateTimestamp() {
            var customer = createValidB2BCustomer();
            var before = customer.updatedAt();
            var contract = createValidFrameContract();

            customer.setFrameContract(contract);

            assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
        }
    }

    // ==================== Preferences ====================

    @Nested
    @DisplayName("setPreferences()")
    class Preferences {

        @Test
        @DisplayName("should set preferences")
        void shouldSetPreferences() {
            var customer = createValidCustomer();

            customer.setPreferences(Set.of(CustomerPreference.BIO, CustomerPreference.REGIONAL));

            assertThat(customer.preferences()).containsExactlyInAnyOrder(
                    CustomerPreference.BIO, CustomerPreference.REGIONAL
            );
        }

        @Test
        @DisplayName("should replace existing preferences")
        void shouldReplaceExistingPreferences() {
            var customer = createValidCustomer();
            customer.setPreferences(Set.of(CustomerPreference.BIO, CustomerPreference.HALAL));

            customer.setPreferences(Set.of(CustomerPreference.KOSHER));

            assertThat(customer.preferences()).containsExactly(CustomerPreference.KOSHER);
        }

        @Test
        @DisplayName("should clear preferences with empty set")
        void shouldClearPreferencesWithEmptySet() {
            var customer = createValidCustomer();
            customer.setPreferences(Set.of(CustomerPreference.BIO));

            customer.setPreferences(Set.of());

            assertThat(customer.preferences()).isEmpty();
        }

        @Test
        @DisplayName("should add single preference")
        void shouldAddSinglePreference() {
            var customer = createValidCustomer();

            customer.addPreference(CustomerPreference.BIO);

            assertThat(customer.preferences()).contains(CustomerPreference.BIO);
        }

        @Test
        @DisplayName("should remove single preference")
        void shouldRemoveSinglePreference() {
            var customer = createValidCustomer();
            customer.addPreference(CustomerPreference.BIO);
            customer.addPreference(CustomerPreference.REGIONAL);

            customer.removePreference(CustomerPreference.BIO);

            assertThat(customer.preferences()).containsExactly(CustomerPreference.REGIONAL);
        }

        @Test
        @DisplayName("setPreferences should update updatedAt")
        void shouldUpdateTimestamp() {
            var customer = createValidCustomer();
            var before = customer.updatedAt();

            customer.setPreferences(Set.of(CustomerPreference.TIERWOHL));

            assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
        }

        @Test
        @DisplayName("preferences should return unmodifiable set")
        void shouldReturnUnmodifiableSet() {
            var customer = createValidCustomer();

            var prefs = customer.preferences();

            org.junit.jupiter.api.Assertions.assertThrows(
                    UnsupportedOperationException.class,
                    () -> prefs.add(CustomerPreference.BIO)
            );
        }
    }

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

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

        @Test
        @DisplayName("should be equal if same ID")
        void shouldBeEqualBySameId() {
            var id = CustomerId.generate();
            var name = new CustomerName("Test");
            var address = Address.create("Str.", null, "12345", "City", "DE").unsafeGetValue();
            var contact = new de.effigenix.shared.common.ContactInfo("+49 1", null, null);
            var now = java.time.OffsetDateTime.now();

            var c1 = Customer.reconstitute(id, name, CustomerType.B2C, address, contact,
                    null, List.of(), null, Set.of(), CustomerStatus.ACTIVE, now, now);
            var c2 = Customer.reconstitute(id, new CustomerName("Anderer Name"), CustomerType.B2B, address, contact,
                    null, List.of(), null, Set.of(), CustomerStatus.INACTIVE, now, now);

            assertThat(c1).isEqualTo(c2);
            assertThat(c1.hashCode()).isEqualTo(c2.hashCode());
        }

        @Test
        @DisplayName("should not be equal if different ID")
        void shouldNotBeEqualByDifferentId() {
            var c1 = createValidCustomer();
            var c2 = createValidCustomer();

            assertThat(c1).isNotEqualTo(c2);
        }

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

            assertThat(customer).isNotEqualTo(null);
        }

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

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

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

            assertThat(customer).isEqualTo(customer);
        }
    }

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

    private Customer createValidCustomer() {
        var draft = new CustomerDraft(
                "Testkunde B2C", CustomerType.B2C,
                "+49 123 456", "test@example.com", null,
                "Teststr.", "1", "12345", "Berlin", "DE",
                null, null
        );
        return Customer.create(draft).unsafeGetValue();
    }

    private Customer createValidB2BCustomer() {
        var draft = new CustomerDraft(
                "Testkunde B2B GmbH", CustomerType.B2B,
                "+49 800 999", "b2b@example.com", "Herr Test",
                "Industriestr.", "42", "80331", "Muenchen", "DE",
                30, "30 Tage netto"
        );
        return Customer.create(draft).unsafeGetValue();
    }

    private FrameContract createValidFrameContract() {
        var lineItem = new ContractLineItem(
                new ArticleId("ART-001"),
                Money.euro(new BigDecimal("10.00")),
                new BigDecimal("100"),
                Unit.KG
        );
        return FrameContract.create(
                LocalDate.now().minusDays(1), LocalDate.now().plusDays(30),
                DeliveryRhythm.WEEKLY, List.of(lineItem)
        ).unsafeGetValue();
    }
}
