package de.effigenix.domain.masterdata;

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

import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;

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

class SupplierTest {

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

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

        @Test
        @DisplayName("should create Supplier with mandatory fields only")
        void shouldCreateWithMandatoryFieldsOnly() {
            var draft = new SupplierDraft(
                    "Lieferant A", "+49 123 456", null, null,
                    null, null, null, null, null, null, null);

            var result = Supplier.create(draft);

            assertThat(result.isSuccess()).isTrue();
            var supplier = result.unsafeGetValue();
            assertThat(supplier.id()).isNotNull();
            assertThat(supplier.name().value()).isEqualTo("Lieferant A");
            assertThat(supplier.contactInfo().phone()).isEqualTo("+49 123 456");
            assertThat(supplier.address()).isNull();
            assertThat(supplier.paymentTerms()).isNull();
            assertThat(supplier.certificates()).isEmpty();
            assertThat(supplier.rating()).isNull();
            assertThat(supplier.status()).isEqualTo(SupplierStatus.ACTIVE);
            assertThat(supplier.createdAt()).isNotNull();
            assertThat(supplier.updatedAt()).isNotNull();
        }

        @Test
        @DisplayName("should create Supplier with all optional fields")
        void shouldCreateWithAllOptionalFields() {
            var draft = new SupplierDraft(
                    "Lieferant B", "+49 999 888", "info@lieferant.de", "Max Mustermann",
                    "Hauptstr.", "42", "12345", "Berlin", "DE", 30, "Netto 30 Tage");

            var result = Supplier.create(draft);

            assertThat(result.isSuccess()).isTrue();
            var supplier = result.unsafeGetValue();
            assertThat(supplier.name().value()).isEqualTo("Lieferant B");
            assertThat(supplier.contactInfo().phone()).isEqualTo("+49 999 888");
            assertThat(supplier.contactInfo().email()).isEqualTo("info@lieferant.de");
            assertThat(supplier.contactInfo().contactPerson()).isEqualTo("Max Mustermann");
            assertThat(supplier.address()).isNotNull();
            assertThat(supplier.address().street()).isEqualTo("Hauptstr.");
            assertThat(supplier.address().city()).isEqualTo("Berlin");
            assertThat(supplier.address().country()).isEqualTo("DE");
            assertThat(supplier.paymentTerms()).isNotNull();
            assertThat(supplier.paymentTerms().paymentDueDays()).isEqualTo(30);
        }

        @Test
        @DisplayName("should fail when name is null")
        void shouldFailWhenNameNull() {
            var draft = new SupplierDraft(
                    null, "+49 123 456", null, null,
                    null, null, null, null, null, null, null);

            var result = Supplier.create(draft);

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

        @Test
        @DisplayName("should fail when name is blank")
        void shouldFailWhenNameBlank() {
            var draft = new SupplierDraft(
                    "   ", "+49 123 456", null, null,
                    null, null, null, null, null, null, null);

            var result = Supplier.create(draft);

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

        @Test
        @DisplayName("should fail when name exceeds 200 chars")
        void shouldFailWhenNameTooLong() {
            var longName = "A".repeat(201);
            var draft = new SupplierDraft(
                    longName, "+49 123 456", null, null,
                    null, null, null, null, null, null, null);

            var result = Supplier.create(draft);

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

        @Test
        @DisplayName("should accept name with exactly 200 chars")
        void shouldAcceptNameWith200Chars() {
            var name = "A".repeat(200);
            var draft = new SupplierDraft(
                    name, "+49 123 456", null, null,
                    null, null, null, null, null, null, null);

            var result = Supplier.create(draft);

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

        @Test
        @DisplayName("should fail when email format is invalid")
        void shouldFailWhenEmailInvalid() {
            var draft = new SupplierDraft(
                    "Lieferant", "+49 123 456", "invalid-email", null,
                    null, null, null, null, null, null, null);

            var result = Supplier.create(draft);

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

        @Test
        @DisplayName("should fail when address is incomplete (street without city)")
        void shouldFailWhenAddressIncomplete() {
            var draft = new SupplierDraft(
                    "Lieferant", "+49 123 456", null, null,
                    "Hauptstr.", null, null, null, null, null, null);

            var result = Supplier.create(draft);

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

        @Test
        @DisplayName("should fail when country code is invalid")
        void shouldFailWhenCountryCodeInvalid() {
            var draft = new SupplierDraft(
                    "Lieferant", "+49 123 456", null, null,
                    "Hauptstr.", "1", "12345", "Berlin", "Deutschland", null, null);

            var result = Supplier.create(draft);

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

        @Test
        @DisplayName("should fail when paymentDueDays is negative")
        void shouldFailWhenPaymentDueDaysNegative() {
            var draft = new SupplierDraft(
                    "Lieferant", "+49 123 456", null, null,
                    null, null, null, null, null, -1, null);

            var result = Supplier.create(draft);

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

        @Test
        @DisplayName("should not create address when street and city are both null")
        void shouldNotCreateAddressWhenBothNull() {
            var draft = new SupplierDraft(
                    "Lieferant", "+49 123 456", null, null,
                    null, null, "12345", null, "DE", null, null);

            var result = Supplier.create(draft);

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

        @Test
        @DisplayName("should trigger address creation when city is set")
        void shouldTriggerAddressCreationWhenCitySet() {
            var draft = new SupplierDraft(
                    "Lieferant", "+49 123 456", null, null,
                    null, null, null, "Berlin", null, null, null);

            var result = Supplier.create(draft);

            // Address creation triggered but street is null -> validation failure
            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
        }
    }

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

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

        @Test
        @DisplayName("should update name")
        void shouldUpdateName() {
            var supplier = createValidSupplier();
            var updateDraft = new SupplierUpdateDraft(
                    "Neuer Name", null, null, null,
                    null, null, null, null, null, null, null);

            var result = supplier.update(updateDraft);

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

        @Test
        @DisplayName("should update contact info")
        void shouldUpdateContactInfo() {
            var supplier = createValidSupplier();
            var updateDraft = new SupplierUpdateDraft(
                    null, "+49 111 222", "new@mail.de", "Erika Muster",
                    null, null, null, null, null, null, null);

            var result = supplier.update(updateDraft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(supplier.contactInfo().phone()).isEqualTo("+49 111 222");
            assertThat(supplier.contactInfo().email()).isEqualTo("new@mail.de");
            assertThat(supplier.contactInfo().contactPerson()).isEqualTo("Erika Muster");
        }

        @Test
        @DisplayName("should update address")
        void shouldUpdateAddress() {
            var supplier = createValidSupplier();
            var updateDraft = new SupplierUpdateDraft(
                    null, null, null, null,
                    "Neue Str.", "10", "54321", "Hamburg", "DE", null, null);

            var result = supplier.update(updateDraft);

            assertThat(result.isSuccess()).isTrue();
            assertThat(supplier.address()).isNotNull();
            assertThat(supplier.address().street()).isEqualTo("Neue Str.");
            assertThat(supplier.address().city()).isEqualTo("Hamburg");
        }

        @Test
        @DisplayName("should update payment terms")
        void shouldUpdatePaymentTerms() {
            var supplier = createValidSupplier();
            var updateDraft = new SupplierUpdateDraft(
                    null, null, null, null,
                    null, null, null, null, null, 60, "Netto 60 Tage");

            var result = supplier.update(updateDraft);

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

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

            var result = supplier.update(updateDraft);

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

        @Test
        @DisplayName("should fail update when name is blank")
        void shouldFailUpdateWhenNameBlank() {
            var supplier = createValidSupplier();
            var updateDraft = new SupplierUpdateDraft(
                    "", null, null, null,
                    null, null, null, null, null, null, null);

            var result = supplier.update(updateDraft);

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

        @Test
        @DisplayName("should fail update when email is invalid")
        void shouldFailUpdateWhenEmailInvalid() {
            var supplier = createValidSupplier();
            var updateDraft = new SupplierUpdateDraft(
                    null, "+49 123 456", "not-an-email", null,
                    null, null, null, null, null, null, null);

            var result = supplier.update(updateDraft);

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

        @Test
        @DisplayName("should update updatedAt timestamp")
        void shouldUpdateTimestamp() {
            var supplier = createValidSupplier();
            var before = supplier.updatedAt();

            supplier.update(new SupplierUpdateDraft(
                    "Geaendert", null, null, null,
                    null, null, null, null, null, null, null));

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

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

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

        @Test
        @DisplayName("should deactivate active supplier")
        void shouldDeactivate() {
            var supplier = createValidSupplier();
            assertThat(supplier.status()).isEqualTo(SupplierStatus.ACTIVE);

            supplier.deactivate();

            assertThat(supplier.status()).isEqualTo(SupplierStatus.INACTIVE);
        }

        @Test
        @DisplayName("should activate inactive supplier")
        void shouldActivate() {
            var supplier = createValidSupplier();
            supplier.deactivate();
            assertThat(supplier.status()).isEqualTo(SupplierStatus.INACTIVE);

            supplier.activate();

            assertThat(supplier.status()).isEqualTo(SupplierStatus.ACTIVE);
        }

        @Test
        @DisplayName("should update updatedAt on deactivate")
        void shouldUpdateTimestampOnDeactivate() {
            var supplier = createValidSupplier();
            var before = supplier.updatedAt();

            supplier.deactivate();

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

        @Test
        @DisplayName("should update updatedAt on activate")
        void shouldUpdateTimestampOnActivate() {
            var supplier = createValidSupplier();
            supplier.deactivate();
            var before = supplier.updatedAt();

            supplier.activate();

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

    // ==================== Rate ====================

    @Nested
    @DisplayName("rate()")
    class Rate {

        @Test
        @DisplayName("should set rating")
        void shouldSetRating() {
            var supplier = createValidSupplier();
            var rating = new SupplierRating(4, 3, 5);

            supplier.rate(rating);

            assertThat(supplier.rating()).isEqualTo(rating);
            assertThat(supplier.rating().qualityScore()).isEqualTo(4);
            assertThat(supplier.rating().deliveryScore()).isEqualTo(3);
            assertThat(supplier.rating().priceScore()).isEqualTo(5);
        }

        @Test
        @DisplayName("should overwrite existing rating")
        void shouldOverwriteExistingRating() {
            var supplier = createValidSupplier();
            supplier.rate(new SupplierRating(1, 1, 1));

            var newRating = new SupplierRating(5, 5, 5);
            supplier.rate(newRating);

            assertThat(supplier.rating()).isEqualTo(newRating);
        }

        @Test
        @DisplayName("should update updatedAt on rate")
        void shouldUpdateTimestampOnRate() {
            var supplier = createValidSupplier();
            var before = supplier.updatedAt();

            supplier.rate(new SupplierRating(3, 3, 3));

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

    // ==================== Certificates ====================

    @Nested
    @DisplayName("addCertificate() / removeCertificate()")
    class Certificates {

        @Test
        @DisplayName("should add certificate")
        void shouldAddCertificate() {
            var supplier = createValidSupplier();
            var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));

            supplier.addCertificate(cert);

            assertThat(supplier.certificates()).hasSize(1);
            assertThat(supplier.certificates().getFirst()).isEqualTo(cert);
        }

        @Test
        @DisplayName("should not add duplicate certificate")
        void shouldNotAddDuplicate() {
            var supplier = createValidSupplier();
            var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));

            supplier.addCertificate(cert);
            supplier.addCertificate(cert);

            assertThat(supplier.certificates()).hasSize(1);
        }

        @Test
        @DisplayName("should add different certificates")
        void shouldAddDifferentCertificates() {
            var supplier = createValidSupplier();
            var cert1 = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
            var cert2 = new QualityCertificate("ISO 22000", "Dekra", LocalDate.of(2025, 6, 1), LocalDate.of(2027, 6, 1));

            supplier.addCertificate(cert1);
            supplier.addCertificate(cert2);

            assertThat(supplier.certificates()).hasSize(2);
        }

        @Test
        @DisplayName("should remove certificate")
        void shouldRemoveCertificate() {
            var supplier = createValidSupplier();
            var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
            supplier.addCertificate(cert);

            supplier.removeCertificate(cert);

            assertThat(supplier.certificates()).isEmpty();
        }

        @Test
        @DisplayName("should not fail when removing non-existent certificate")
        void shouldNotFailWhenRemovingNonExistent() {
            var supplier = createValidSupplier();
            var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));

            supplier.removeCertificate(cert);

            assertThat(supplier.certificates()).isEmpty();
        }

        @Test
        @DisplayName("should return unmodifiable certificate list")
        void shouldReturnUnmodifiableCertificateList() {
            var supplier = createValidSupplier();
            var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
            supplier.addCertificate(cert);

            var certs = supplier.certificates();

            org.junit.jupiter.api.Assertions.assertThrows(
                    UnsupportedOperationException.class,
                    () -> certs.add(new QualityCertificate("HACCP", "Bureau Veritas", null, null))
            );
        }

        @Test
        @DisplayName("should update updatedAt on addCertificate")
        void shouldUpdateTimestampOnAdd() {
            var supplier = createValidSupplier();
            var before = supplier.updatedAt();
            var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));

            supplier.addCertificate(cert);

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

        @Test
        @DisplayName("should update updatedAt on removeCertificate")
        void shouldUpdateTimestampOnRemove() {
            var supplier = createValidSupplier();
            var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
            supplier.addCertificate(cert);
            var before = supplier.updatedAt();

            supplier.removeCertificate(cert);

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

    // ==================== Expired / Expiring Certificates ====================

    @Nested
    @DisplayName("expiredCertificates() / certificatesExpiringSoon()")
    class CertificateQueries {

        @Test
        @DisplayName("should return expired certificates")
        void shouldReturnExpiredCertificates() {
            var supplier = createValidSupplier();
            var expired = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2020, 1, 1), LocalDate.of(2022, 1, 1));
            var valid = new QualityCertificate("HACCP", "Dekra", LocalDate.of(2025, 1, 1), LocalDate.of(2030, 1, 1));
            supplier.addCertificate(expired);
            supplier.addCertificate(valid);

            var result = supplier.expiredCertificates();

            assertThat(result).hasSize(1);
            assertThat(result.getFirst().certificateType()).isEqualTo("ISO 9001");
        }

        @Test
        @DisplayName("should return empty list when no certificates are expired")
        void shouldReturnEmptyWhenNoneExpired() {
            var supplier = createValidSupplier();
            var valid = new QualityCertificate("HACCP", "Dekra", LocalDate.of(2025, 1, 1), LocalDate.of(2030, 1, 1));
            supplier.addCertificate(valid);

            assertThat(supplier.expiredCertificates()).isEmpty();
        }

        @Test
        @DisplayName("should return certificates expiring within given days")
        void shouldReturnCertificatesExpiringSoon() {
            var supplier = createValidSupplier();
            var expiringSoon = new QualityCertificate("ISO 9001", "TUV",
                    LocalDate.of(2025, 1, 1), LocalDate.now().plusDays(15));
            var farFuture = new QualityCertificate("HACCP", "Dekra",
                    LocalDate.of(2025, 1, 1), LocalDate.now().plusDays(365));
            supplier.addCertificate(expiringSoon);
            supplier.addCertificate(farFuture);

            var result = supplier.certificatesExpiringSoon(30);

            assertThat(result).hasSize(1);
            assertThat(result.getFirst().certificateType()).isEqualTo("ISO 9001");
        }

        @Test
        @DisplayName("should not include certificates without validUntil in expiring soon")
        void shouldNotIncludeCertsWithoutValidUntil() {
            var supplier = createValidSupplier();
            var noExpiry = new QualityCertificate("Bio", "Bioland", null, null);
            supplier.addCertificate(noExpiry);

            assertThat(supplier.certificatesExpiringSoon(30)).isEmpty();
        }
    }

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

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

        @Test
        @DisplayName("should be equal if same ID")
        void shouldBeEqualBySameId() {
            var id = SupplierId.generate();
            var now = OffsetDateTime.now(ZoneOffset.UTC);
            var name = new SupplierName("Lieferant");
            var contactInfo = new de.effigenix.shared.common.ContactInfo("+49 123", null, null);

            var s1 = Supplier.reconstitute(id, name, null, contactInfo, null, List.of(), null, SupplierStatus.ACTIVE, now, now);
            var s2 = Supplier.reconstitute(id, new SupplierName("Anderer Name"), null, contactInfo, null, List.of(), null, SupplierStatus.INACTIVE, now, now);

            assertThat(s1).isEqualTo(s2);
            assertThat(s1.hashCode()).isEqualTo(s2.hashCode());
        }

        @Test
        @DisplayName("should not be equal if different ID")
        void shouldNotBeEqualByDifferentId() {
            var s1 = createValidSupplier();
            var s2 = createValidSupplier();

            assertThat(s1).isNotEqualTo(s2);
        }

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

            assertThat(supplier).isEqualTo(supplier);
        }

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

            assertThat(supplier).isNotEqualTo(null);
        }

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

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

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

    private Supplier createValidSupplier() {
        var draft = new SupplierDraft(
                "Test Lieferant", "+49 123 456", "test@example.com", "Max Muster",
                null, null, null, null, null, null, null);
        return Supplier.create(draft).unsafeGetValue();
    }
}
