package de.effigenix.application.masterdata;

import de.effigenix.application.masterdata.customer.*;
import de.effigenix.application.masterdata.customer.command.*;
import de.effigenix.domain.masterdata.*;
import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.domain.masterdata.customer.*;
import de.effigenix.shared.common.*;
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.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
import java.util.Set;
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("Customer Use Cases")
class CustomerUseCaseTest {

    @Mock private CustomerRepository customerRepository;
    @Mock private UnitOfWork unitOfWork;

    private ActorId performedBy;

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

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

    private static Customer existingB2BCustomer(String id) {
        return Customer.reconstitute(
                CustomerId.of(id),
                new CustomerName("Metzgerei Müller"),
                CustomerType.B2B,
                new Address("Hauptstr.", "10", "80331", "München", "DE"),
                new ContactInfo("+49 89 12345", "info@mueller.de", "Hans Müller"),
                new PaymentTerms(30, "30 Tage netto"),
                List.of(),
                null,
                Set.of(),
                CustomerStatus.ACTIVE,
                OffsetDateTime.now(ZoneOffset.UTC).minusDays(10),
                OffsetDateTime.now(ZoneOffset.UTC).minusDays(1)
        );
    }

    private static Customer existingB2CCustomer(String id) {
        return Customer.reconstitute(
                CustomerId.of(id),
                new CustomerName("Max Mustermann"),
                CustomerType.B2C,
                new Address("Berliner Str.", "5", "10115", "Berlin", "DE"),
                new ContactInfo("+49 30 98765", null, null),
                null,
                List.of(),
                null,
                Set.of(),
                CustomerStatus.ACTIVE,
                OffsetDateTime.now(ZoneOffset.UTC).minusDays(5),
                OffsetDateTime.now(ZoneOffset.UTC).minusDays(1)
        );
    }

    private static Customer inactiveCustomer(String id) {
        return Customer.reconstitute(
                CustomerId.of(id),
                new CustomerName("Alte Bäckerei"),
                CustomerType.B2B,
                new Address("Bahnhofstr.", "1", "60329", "Frankfurt", "DE"),
                new ContactInfo("+49 69 11111", null, null),
                null,
                List.of(),
                null,
                Set.of(),
                CustomerStatus.INACTIVE,
                OffsetDateTime.now(ZoneOffset.UTC).minusDays(30),
                OffsetDateTime.now(ZoneOffset.UTC).minusDays(2)
        );
    }

    private static CreateCustomerCommand validCreateCommand() {
        return new CreateCustomerCommand(
                "Neuer Kunde GmbH", CustomerType.B2B,
                "Industriestr.", "42", "70173", "Stuttgart", "DE",
                "+49 711 55555", "kontakt@neuer-kunde.de", "Anna Schmidt",
                14, "14 Tage netto"
        );
    }

    // ==================== CreateCustomer ====================

    @Nested
    @DisplayName("CreateCustomer")
    class CreateCustomerTests {

        private CreateCustomer createCustomer;

        @BeforeEach
        void setUp() {
            createCustomer = new CreateCustomer(customerRepository, unitOfWork);
        }

        @Test
        @DisplayName("should create customer successfully")
        void shouldCreateCustomerSuccessfully() {
            var cmd = validCreateCommand();
            when(customerRepository.existsByName(any())).thenReturn(Result.success(false));
            when(customerRepository.save(any())).thenReturn(Result.success(null));

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

            assertThat(result.isSuccess()).isTrue();
            var customer = result.unsafeGetValue();
            assertThat(customer.name().value()).isEqualTo("Neuer Kunde GmbH");
            assertThat(customer.type()).isEqualTo(CustomerType.B2B);
            assertThat(customer.status()).isEqualTo(CustomerStatus.ACTIVE);
            assertThat(customer.billingAddress().street()).isEqualTo("Industriestr.");
            assertThat(customer.contactInfo().phone()).isEqualTo("+49 711 55555");
            verify(customerRepository).save(any(Customer.class));
        }

        @Test
        @DisplayName("should fail when customer name already exists")
        void shouldFailWhenNameAlreadyExists() {
            var cmd = validCreateCommand();
            when(customerRepository.existsByName(any())).thenReturn(Result.success(true));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNameAlreadyExists.class);
            verify(customerRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with ValidationFailure when name is blank")
        void shouldFailWhenNameIsBlank() {
            var cmd = new CreateCustomerCommand(
                    "", CustomerType.B2B,
                    "Str.", null, "12345", "City", "DE",
                    "+49 1", null, null, null, null
            );

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

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

        @Test
        @DisplayName("should fail with RepositoryFailure when existsByName fails")
        void shouldFailWhenExistsByNameFails() {
            var cmd = validCreateCommand();
            when(customerRepository.existsByName(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

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

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var cmd = validCreateCommand();
            when(customerRepository.existsByName(any())).thenReturn(Result.success(false));
            when(customerRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

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

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

        @Test
        @DisplayName("should create customer without optional payment terms")
        void shouldCreateWithoutPaymentTerms() {
            var cmd = new CreateCustomerCommand(
                    "Einfach GmbH", CustomerType.B2C,
                    "Str.", null, "12345", "City", "DE",
                    "+49 1", null, null, null, null
            );
            when(customerRepository.existsByName(any())).thenReturn(Result.success(false));
            when(customerRepository.save(any())).thenReturn(Result.success(null));

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

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

    // ==================== UpdateCustomer ====================

    @Nested
    @DisplayName("UpdateCustomer")
    class UpdateCustomerTests {

        private UpdateCustomer updateCustomer;

        @BeforeEach
        void setUp() {
            updateCustomer = new UpdateCustomer(customerRepository, unitOfWork);
        }

        @Test
        @DisplayName("should update customer successfully")
        void shouldUpdateCustomerSuccessfully() {
            var customerId = "cust-1";
            var cmd = new UpdateCustomerCommand(
                    customerId, "Neuer Name", "Neue Str.", "99", "99999", "Neustadt", "DE",
                    "+49 999 0000", "neu@test.de", "Neue Person", 60, "60 Tage netto"
            );
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
            when(customerRepository.save(any())).thenReturn(Result.success(null));

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

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().name().value()).isEqualTo("Neuer Name");
            verify(customerRepository).save(any(Customer.class));
        }

        @Test
        @DisplayName("should fail with CustomerNotFound when customer does not exist")
        void shouldFailWhenCustomerNotFound() {
            var cmd = new UpdateCustomerCommand(
                    "nonexistent", null, null, null, null, null, null,
                    null, null, null, null, null
            );
            when(customerRepository.findById(CustomerId.of("nonexistent")))
                    .thenReturn(Result.success(Optional.empty()));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
            verify(customerRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var cmd = new UpdateCustomerCommand(
                    "cust-1", null, null, null, null, null, null,
                    null, null, null, null, null
            );
            when(customerRepository.findById(CustomerId.of("cust-1")))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));

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

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var customerId = "cust-1";
            var cmd = new UpdateCustomerCommand(
                    customerId, "Updated", null, null, null, null, null,
                    null, null, null, null, null
            );
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
            when(customerRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

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

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

        @Test
        @DisplayName("should fail with ValidationFailure when updated name is blank")
        void shouldFailWhenUpdatedNameIsBlank() {
            var customerId = "cust-1";
            var cmd = new UpdateCustomerCommand(
                    customerId, "", null, null, null, null, null,
                    null, null, null, null, null
            );
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));

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

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

    // ==================== GetCustomer ====================

    @Nested
    @DisplayName("GetCustomer")
    class GetCustomerTests {

        private GetCustomer getCustomer;

        @BeforeEach
        void setUp() {
            getCustomer = new GetCustomer(customerRepository);
        }

        @Test
        @DisplayName("should return customer when found")
        void shouldReturnCustomerWhenFound() {
            var customerId = CustomerId.of("cust-1");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer("cust-1"))));

            var result = getCustomer.execute(customerId);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().id()).isEqualTo(customerId);
        }

        @Test
        @DisplayName("should fail with CustomerNotFound when not found")
        void shouldFailWhenNotFound() {
            var customerId = CustomerId.of("nonexistent");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.success(Optional.empty()));

            var result = getCustomer.execute(customerId);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when repository fails")
        void shouldFailWhenRepositoryFails() {
            var customerId = CustomerId.of("cust-1");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = getCustomer.execute(customerId);

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

    // ==================== ListCustomers ====================

    @Nested
    @DisplayName("ListCustomers")
    class ListCustomersTests {

        private ListCustomers listCustomers;

        @BeforeEach
        void setUp() {
            listCustomers = new ListCustomers(customerRepository);
        }

        @Test
        @DisplayName("should return all customers")
        void shouldReturnAllCustomers() {
            var customers = List.of(existingB2BCustomer("c1"), existingB2CCustomer("c2"));
            when(customerRepository.findAll()).thenReturn(Result.success(customers));

            var result = listCustomers.execute();

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

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

            var result = listCustomers.execute();

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

        @Test
        @DisplayName("should fail with RepositoryFailure when findAll fails")
        void shouldFailWhenFindAllFails() {
            when(customerRepository.findAll())
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));

            var result = listCustomers.execute();

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

        @Test
        @DisplayName("should return customers by type")
        void shouldReturnCustomersByType() {
            var b2bCustomers = List.of(existingB2BCustomer("c1"));
            when(customerRepository.findByType(CustomerType.B2B)).thenReturn(Result.success(b2bCustomers));

            var result = listCustomers.executeByType(CustomerType.B2B);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue()).hasSize(1);
            assertThat(result.unsafeGetValue().getFirst().type()).isEqualTo(CustomerType.B2B);
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findByType fails")
        void shouldFailWhenFindByTypeFails() {
            when(customerRepository.findByType(CustomerType.B2B))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("error")));

            var result = listCustomers.executeByType(CustomerType.B2B);

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

        @Test
        @DisplayName("should return customers by status")
        void shouldReturnCustomersByStatus() {
            var activeCustomers = List.of(existingB2BCustomer("c1"), existingB2CCustomer("c2"));
            when(customerRepository.findByStatus(CustomerStatus.ACTIVE)).thenReturn(Result.success(activeCustomers));

            var result = listCustomers.executeByStatus(CustomerStatus.ACTIVE);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when findByStatus fails")
        void shouldFailWhenFindByStatusFails() {
            when(customerRepository.findByStatus(CustomerStatus.ACTIVE))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("error")));

            var result = listCustomers.executeByStatus(CustomerStatus.ACTIVE);

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

    // ==================== ActivateCustomer ====================

    @Nested
    @DisplayName("ActivateCustomer")
    class ActivateCustomerTests {

        private ActivateCustomer activateCustomer;

        @BeforeEach
        void setUp() {
            activateCustomer = new ActivateCustomer(customerRepository, unitOfWork);
        }

        @Test
        @DisplayName("should activate an inactive customer")
        void shouldActivateInactiveCustomer() {
            var customerId = CustomerId.of("cust-1");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.success(Optional.of(inactiveCustomer("cust-1"))));
            when(customerRepository.save(any())).thenReturn(Result.success(null));

            var result = activateCustomer.execute(customerId, performedBy);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().status()).isEqualTo(CustomerStatus.ACTIVE);
            verify(customerRepository).save(any(Customer.class));
        }

        @Test
        @DisplayName("should fail with CustomerNotFound when customer does not exist")
        void shouldFailWhenCustomerNotFound() {
            var customerId = CustomerId.of("nonexistent");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.success(Optional.empty()));

            var result = activateCustomer.execute(customerId, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
            verify(customerRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var customerId = CustomerId.of("cust-1");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = activateCustomer.execute(customerId, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var customerId = CustomerId.of("cust-1");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.success(Optional.of(inactiveCustomer("cust-1"))));
            when(customerRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

            var result = activateCustomer.execute(customerId, performedBy);

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

    // ==================== DeactivateCustomer ====================

    @Nested
    @DisplayName("DeactivateCustomer")
    class DeactivateCustomerTests {

        private DeactivateCustomer deactivateCustomer;

        @BeforeEach
        void setUp() {
            deactivateCustomer = new DeactivateCustomer(customerRepository, unitOfWork);
        }

        @Test
        @DisplayName("should deactivate an active customer")
        void shouldDeactivateActiveCustomer() {
            var customerId = CustomerId.of("cust-1");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer("cust-1"))));
            when(customerRepository.save(any())).thenReturn(Result.success(null));

            var result = deactivateCustomer.execute(customerId, performedBy);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().status()).isEqualTo(CustomerStatus.INACTIVE);
            verify(customerRepository).save(any(Customer.class));
        }

        @Test
        @DisplayName("should fail with CustomerNotFound when customer does not exist")
        void shouldFailWhenCustomerNotFound() {
            var customerId = CustomerId.of("nonexistent");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.success(Optional.empty()));

            var result = deactivateCustomer.execute(customerId, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
            verify(customerRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var customerId = CustomerId.of("cust-1");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = deactivateCustomer.execute(customerId, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var customerId = CustomerId.of("cust-1");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer("cust-1"))));
            when(customerRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

            var result = deactivateCustomer.execute(customerId, performedBy);

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

    // ==================== AddDeliveryAddress ====================

    @Nested
    @DisplayName("AddDeliveryAddress")
    class AddDeliveryAddressTests {

        private AddDeliveryAddress addDeliveryAddress;

        @BeforeEach
        void setUp() {
            addDeliveryAddress = new AddDeliveryAddress(customerRepository, unitOfWork);
        }

        @Test
        @DisplayName("should add delivery address successfully")
        void shouldAddDeliveryAddressSuccessfully() {
            var customerId = "cust-1";
            var cmd = new AddDeliveryAddressCommand(
                    customerId, "Lager Süd", "Lagerstr.", "1", "80000", "München", "DE",
                    "Max Lager", "Tor 3 benutzen"
            );
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
            when(customerRepository.save(any())).thenReturn(Result.success(null));

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

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().deliveryAddresses()).hasSize(1);
            assertThat(result.unsafeGetValue().deliveryAddresses().getFirst().label()).isEqualTo("Lager Süd");
            verify(customerRepository).save(any(Customer.class));
        }

        @Test
        @DisplayName("should fail with CustomerNotFound when customer does not exist")
        void shouldFailWhenCustomerNotFound() {
            var cmd = new AddDeliveryAddressCommand(
                    "nonexistent", "Label", "Str.", null, "12345", "City", "DE", null, null
            );
            when(customerRepository.findById(CustomerId.of("nonexistent")))
                    .thenReturn(Result.success(Optional.empty()));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
            verify(customerRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with ValidationFailure when address is invalid")
        void shouldFailWhenAddressIsInvalid() {
            var customerId = "cust-1";
            var cmd = new AddDeliveryAddressCommand(
                    customerId, "Label", "", null, "", "", "XX",
                    null, null
            );
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));

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

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

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var cmd = new AddDeliveryAddressCommand(
                    "cust-1", "Label", "Str.", null, "12345", "City", "DE", null, null
            );
            when(customerRepository.findById(CustomerId.of("cust-1")))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));

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

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var customerId = "cust-1";
            var cmd = new AddDeliveryAddressCommand(
                    customerId, "Label", "Str.", null, "12345", "City", "DE", null, null
            );
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
            when(customerRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

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

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

    // ==================== RemoveDeliveryAddress ====================

    @Nested
    @DisplayName("RemoveDeliveryAddress")
    class RemoveDeliveryAddressTests {

        private RemoveDeliveryAddress removeDeliveryAddress;

        @BeforeEach
        void setUp() {
            removeDeliveryAddress = new RemoveDeliveryAddress(customerRepository, unitOfWork);
        }

        @Test
        @DisplayName("should remove delivery address successfully")
        void shouldRemoveDeliveryAddressSuccessfully() {
            var customerId = "cust-1";
            var customerWithAddr = Customer.reconstitute(
                    CustomerId.of(customerId),
                    new CustomerName("Metzgerei Müller"),
                    CustomerType.B2B,
                    new Address("Hauptstr.", "10", "80331", "München", "DE"),
                    new ContactInfo("+49 89 12345", null, null),
                    null,
                    List.of(new DeliveryAddress("Lager Süd",
                            new Address("Lagerstr.", "1", "80000", "München", "DE"), null, null)),
                    null, Set.of(), CustomerStatus.ACTIVE,
                    OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
            );
            var cmd = new RemoveDeliveryAddressCommand(customerId, "Lager Süd");
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(customerWithAddr)));
            when(customerRepository.save(any())).thenReturn(Result.success(null));

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

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().deliveryAddresses()).isEmpty();
            verify(customerRepository).save(any(Customer.class));
        }

        @Test
        @DisplayName("should fail with CustomerNotFound when customer does not exist")
        void shouldFailWhenCustomerNotFound() {
            var cmd = new RemoveDeliveryAddressCommand("nonexistent", "Label");
            when(customerRepository.findById(CustomerId.of("nonexistent")))
                    .thenReturn(Result.success(Optional.empty()));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
            verify(customerRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var cmd = new RemoveDeliveryAddressCommand("cust-1", "Label");
            when(customerRepository.findById(CustomerId.of("cust-1")))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));

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

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var customerId = "cust-1";
            var cmd = new RemoveDeliveryAddressCommand(customerId, "Label");
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
            when(customerRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

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

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

    // ==================== SetPreferences ====================

    @Nested
    @DisplayName("SetPreferences")
    class SetPreferencesTests {

        private SetPreferences setPreferences;

        @BeforeEach
        void setUp() {
            setPreferences = new SetPreferences(customerRepository, unitOfWork);
        }

        @Test
        @DisplayName("should set preferences successfully")
        void shouldSetPreferencesSuccessfully() {
            var customerId = "cust-1";
            var prefs = Set.of(CustomerPreference.BIO, CustomerPreference.REGIONAL);
            var cmd = new SetPreferencesCommand(customerId, prefs);
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
            when(customerRepository.save(any())).thenReturn(Result.success(null));

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

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().preferences())
                    .containsExactlyInAnyOrder(CustomerPreference.BIO, CustomerPreference.REGIONAL);
            verify(customerRepository).save(any(Customer.class));
        }

        @Test
        @DisplayName("should clear preferences with empty set")
        void shouldClearPreferencesWithEmptySet() {
            var customerId = "cust-1";
            var cmd = new SetPreferencesCommand(customerId, Set.of());
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
            when(customerRepository.save(any())).thenReturn(Result.success(null));

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

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

        @Test
        @DisplayName("should fail with CustomerNotFound when customer does not exist")
        void shouldFailWhenCustomerNotFound() {
            var cmd = new SetPreferencesCommand("nonexistent", Set.of(CustomerPreference.BIO));
            when(customerRepository.findById(CustomerId.of("nonexistent")))
                    .thenReturn(Result.success(Optional.empty()));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
            verify(customerRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var cmd = new SetPreferencesCommand("cust-1", Set.of());
            when(customerRepository.findById(CustomerId.of("cust-1")))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));

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

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var customerId = "cust-1";
            var cmd = new SetPreferencesCommand(customerId, Set.of(CustomerPreference.HALAL));
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
            when(customerRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

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

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

    // ==================== SetFrameContract ====================

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

        private SetFrameContract setFrameContract;

        @BeforeEach
        void setUp() {
            setFrameContract = new SetFrameContract(customerRepository, unitOfWork);
        }

        private SetFrameContractCommand validFrameContractCommand(String customerId) {
            return new SetFrameContractCommand(
                    customerId,
                    LocalDate.now(),
                    LocalDate.now().plusMonths(12),
                    DeliveryRhythm.WEEKLY,
                    List.of(new SetFrameContractCommand.LineItem(
                            "article-1", new BigDecimal("9.99"), new BigDecimal("100"), Unit.KG
                    ))
            );
        }

        @Test
        @DisplayName("should set frame contract on B2B customer successfully")
        void shouldSetFrameContractOnB2BCustomer() {
            var customerId = "cust-1";
            var cmd = validFrameContractCommand(customerId);
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
            when(customerRepository.save(any())).thenReturn(Result.success(null));

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

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().frameContract()).isNotNull();
            verify(customerRepository).save(any(Customer.class));
        }

        @Test
        @DisplayName("should fail with FrameContractNotAllowed when customer is B2C")
        void shouldFailWhenCustomerIsB2C() {
            var customerId = "cust-b2c";
            var cmd = validFrameContractCommand(customerId);
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2CCustomer(customerId))));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.FrameContractNotAllowed.class);
            verify(customerRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with CustomerNotFound when customer does not exist")
        void shouldFailWhenCustomerNotFound() {
            var cmd = validFrameContractCommand("nonexistent");
            when(customerRepository.findById(CustomerId.of("nonexistent")))
                    .thenReturn(Result.success(Optional.empty()));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
            verify(customerRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with InvalidFrameContract when validUntil is before validFrom")
        void shouldFailWhenDatesInvalid() {
            var customerId = "cust-1";
            var cmd = new SetFrameContractCommand(
                    customerId,
                    LocalDate.now().plusMonths(12),
                    LocalDate.now(), // before validFrom
                    DeliveryRhythm.WEEKLY,
                    List.of(new SetFrameContractCommand.LineItem(
                            "article-1", new BigDecimal("9.99"), new BigDecimal("100"), Unit.KG
                    ))
            );
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.InvalidFrameContract.class);
            verify(customerRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with InvalidFrameContract when line items are empty")
        void shouldFailWhenLineItemsEmpty() {
            var customerId = "cust-1";
            var cmd = new SetFrameContractCommand(
                    customerId, LocalDate.now(), LocalDate.now().plusMonths(6),
                    DeliveryRhythm.MONTHLY, List.of()
            );
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));

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

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.InvalidFrameContract.class);
            verify(customerRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var cmd = validFrameContractCommand("cust-1");
            when(customerRepository.findById(CustomerId.of("cust-1")))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));

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

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var customerId = "cust-1";
            var cmd = validFrameContractCommand(customerId);
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
            when(customerRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

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

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

        @Test
        @DisplayName("should fail with ValidationFailure when price is negative")
        void shouldFailWhenPriceIsNegative() {
            var customerId = "cust-1";
            var cmd = new SetFrameContractCommand(
                    customerId, LocalDate.now(), LocalDate.now().plusMonths(6),
                    DeliveryRhythm.MONTHLY,
                    List.of(new SetFrameContractCommand.LineItem(
                            "article-1", new BigDecimal("-5.00"), new BigDecimal("100"), Unit.KG
                    ))
            );
            when(customerRepository.findById(CustomerId.of(customerId)))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));

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

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

    // ==================== RemoveFrameContract ====================

    @Nested
    @DisplayName("RemoveFrameContract")
    class RemoveFrameContractTests {

        private RemoveFrameContract removeFrameContract;

        @BeforeEach
        void setUp() {
            removeFrameContract = new RemoveFrameContract(customerRepository, unitOfWork);
        }

        @Test
        @DisplayName("should remove frame contract successfully")
        void shouldRemoveFrameContractSuccessfully() {
            var customerId = CustomerId.of("cust-1");
            var customerWithContract = existingB2BCustomer("cust-1");
            // Set a frame contract on the customer via reconstitute
            var contractCustomer = Customer.reconstitute(
                    customerId,
                    new CustomerName("Metzgerei Müller"),
                    CustomerType.B2B,
                    new Address("Hauptstr.", "10", "80331", "München", "DE"),
                    new ContactInfo("+49 89 12345", null, null),
                    null,
                    List.of(),
                    FrameContract.reconstitute(
                            FrameContractId.generate(),
                            LocalDate.now().minusMonths(1),
                            LocalDate.now().plusMonths(11),
                            DeliveryRhythm.WEEKLY,
                            List.of(new ContractLineItem(
                                    ArticleId.of("a1"), Money.euro(new BigDecimal("10.00")),
                                    new BigDecimal("50"), Unit.KG))
                    ),
                    Set.of(), CustomerStatus.ACTIVE,
                    OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
            );
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.success(Optional.of(contractCustomer)));
            when(customerRepository.save(any())).thenReturn(Result.success(null));

            var result = removeFrameContract.execute(customerId, performedBy);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().frameContract()).isNull();
            verify(customerRepository).save(any(Customer.class));
        }

        @Test
        @DisplayName("should succeed even when no frame contract exists")
        void shouldSucceedWhenNoFrameContract() {
            var customerId = CustomerId.of("cust-1");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer("cust-1"))));
            when(customerRepository.save(any())).thenReturn(Result.success(null));

            var result = removeFrameContract.execute(customerId, performedBy);

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

        @Test
        @DisplayName("should fail with CustomerNotFound when customer does not exist")
        void shouldFailWhenCustomerNotFound() {
            var customerId = CustomerId.of("nonexistent");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.success(Optional.empty()));

            var result = removeFrameContract.execute(customerId, performedBy);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
            verify(customerRepository, never()).save(any());
        }

        @Test
        @DisplayName("should fail with RepositoryFailure when findById fails")
        void shouldFailWhenFindByIdFails() {
            var customerId = CustomerId.of("cust-1");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));

            var result = removeFrameContract.execute(customerId, performedBy);

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

        @Test
        @DisplayName("should fail with RepositoryFailure when save fails")
        void shouldFailWhenSaveFails() {
            var customerId = CustomerId.of("cust-1");
            when(customerRepository.findById(customerId))
                    .thenReturn(Result.success(Optional.of(existingB2BCustomer("cust-1"))));
            when(customerRepository.save(any()))
                    .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));

            var result = removeFrameContract.execute(customerId, performedBy);

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