package de.effigenix.shared.common;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static de.effigenix.shared.common.UnitOfMeasure.*;
import static org.assertj.core.api.Assertions.assertThat;

@DisplayName("Quantity Value Object")
class QuantityTest {

    @Nested
    @DisplayName("of() factory")
    class OfFactory {

        @Test
        @DisplayName("should create Quantity with valid amount and UoM")
        void should_CreateQuantity_When_ValidAmountAndUom() {
            var result = Quantity.of(new BigDecimal("10"), KILOGRAM);

            assertThat(result.isSuccess()).isTrue();
            var qty = result.unsafeGetValue();
            assertThat(qty.amount()).isEqualByComparingTo(new BigDecimal("10"));
            assertThat(qty.uom()).isEqualTo(KILOGRAM);
            assertThat(qty.hasDualQuantity()).isFalse();
            assertThat(qty.secondaryAmount()).isNull();
            assertThat(qty.secondaryUom()).isNull();
        }

        @Test
        @DisplayName("should fail when amount is zero")
        void should_Fail_When_AmountIsZero() {
            var result = Quantity.of(BigDecimal.ZERO, KILOGRAM);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.AmountMustBePositive.class);
        }

        @Test
        @DisplayName("should fail when amount is negative")
        void should_Fail_When_AmountIsNegative() {
            var result = Quantity.of(new BigDecimal("-5"), KILOGRAM);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.AmountMustBePositive.class);
        }

        @Test
        @DisplayName("should fail when amount is null")
        void should_Fail_When_AmountIsNull() {
            var result = Quantity.of(null, KILOGRAM);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.AmountMustBePositive.class);
        }
    }

    @Nested
    @DisplayName("dual() factory")
    class DualFactory {

        @Test
        @DisplayName("should create dual Quantity with primary and secondary")
        void should_CreateDualQuantity_When_ValidInputs() {
            var result = Quantity.dual(
                    new BigDecimal("10"), PIECE,
                    new BigDecimal("23"), KILOGRAM);

            assertThat(result.isSuccess()).isTrue();
            var qty = result.unsafeGetValue();
            assertThat(qty.hasDualQuantity()).isTrue();
            assertThat(qty.amount()).isEqualByComparingTo(new BigDecimal("10"));
            assertThat(qty.uom()).isEqualTo(PIECE);
            assertThat(qty.secondaryAmount()).isEqualByComparingTo(new BigDecimal("23"));
            assertThat(qty.secondaryUom()).isEqualTo(KILOGRAM);
        }

        @Test
        @DisplayName("should fail when secondary amount is zero")
        void should_Fail_When_SecondaryAmountIsZero() {
            var result = Quantity.dual(
                    new BigDecimal("10"), PIECE,
                    BigDecimal.ZERO, KILOGRAM);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.SecondaryAmountMustBePositive.class);
        }

        @Test
        @DisplayName("should fail when secondary UoM is null")
        void should_Fail_When_SecondaryUomIsNull() {
            var result = Quantity.dual(
                    new BigDecimal("10"), PIECE,
                    new BigDecimal("23"), null);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.SecondaryUomRequired.class);
        }

        @Test
        @DisplayName("should fail when primary amount is invalid")
        void should_Fail_When_PrimaryAmountInvalid() {
            var result = Quantity.dual(
                    BigDecimal.ZERO, PIECE,
                    new BigDecimal("23"), KILOGRAM);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.AmountMustBePositive.class);
        }
    }

    @Nested
    @DisplayName("Arithmetic operations")
    class Arithmetic {

        @Test
        @DisplayName("should add quantities with same UoM")
        void should_Add_When_SameUom() {
            var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
            var q2 = Quantity.of(new BigDecimal("5"), KILOGRAM).unsafeGetValue();

            var result = q1.add(q2);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().amount()).isEqualByComparingTo(new BigDecimal("15"));
            assertThat(result.unsafeGetValue().uom()).isEqualTo(KILOGRAM);
        }

        @Test
        @DisplayName("should fail to add quantities with different UoM")
        void should_FailAdd_When_DifferentUom() {
            var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
            var q2 = Quantity.of(new BigDecimal("5"), LITER).unsafeGetValue();

            var result = q1.add(q2);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.UnitOfMeasureMismatch.class);
        }

        @Test
        @DisplayName("should subtract quantities with same UoM")
        void should_Subtract_When_SameUom() {
            var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
            var q2 = Quantity.of(new BigDecimal("3"), KILOGRAM).unsafeGetValue();

            var result = q1.subtract(q2);

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().amount()).isEqualByComparingTo(new BigDecimal("7"));
        }

        @Test
        @DisplayName("should fail to subtract when result would not be positive")
        void should_FailSubtract_When_ResultNotPositive() {
            var q1 = Quantity.of(new BigDecimal("5"), KILOGRAM).unsafeGetValue();
            var q2 = Quantity.of(new BigDecimal("5"), KILOGRAM).unsafeGetValue();

            var result = q1.subtract(q2);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.AmountMustBePositive.class);
        }

        @Test
        @DisplayName("should fail to subtract with different UoM")
        void should_FailSubtract_When_DifferentUom() {
            var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
            var q2 = Quantity.of(new BigDecimal("3"), GRAM).unsafeGetValue();

            var result = q1.subtract(q2);

            assertThat(result.isFailure()).isTrue();
            assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.UnitOfMeasureMismatch.class);
        }

        @Test
        @DisplayName("should multiply quantity by factor")
        void should_Multiply_When_ValidFactor() {
            var q = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();

            var result = q.multiply(new BigDecimal("2.5"));

            assertThat(result.isSuccess()).isTrue();
            assertThat(result.unsafeGetValue().amount()).isEqualByComparingTo(new BigDecimal("25"));
            assertThat(result.unsafeGetValue().uom()).isEqualTo(KILOGRAM);
        }

        @Test
        @DisplayName("should fail to multiply by zero")
        void should_FailMultiply_When_FactorIsZero() {
            var q = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();

            var result = q.multiply(BigDecimal.ZERO);

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

        @Test
        @DisplayName("should add dual quantities preserving secondary")
        void should_AddDual_When_BothHaveSecondary() {
            var q1 = Quantity.dual(new BigDecimal("10"), PIECE, new BigDecimal("20"), KILOGRAM).unsafeGetValue();
            var q2 = Quantity.dual(new BigDecimal("5"), PIECE, new BigDecimal("10"), KILOGRAM).unsafeGetValue();

            var result = q1.add(q2);

            assertThat(result.isSuccess()).isTrue();
            var sum = result.unsafeGetValue();
            assertThat(sum.amount()).isEqualByComparingTo(new BigDecimal("15"));
            assertThat(sum.secondaryAmount()).isEqualByComparingTo(new BigDecimal("30"));
            assertThat(sum.secondaryUom()).isEqualTo(KILOGRAM);
        }
    }

    @Nested
    @DisplayName("Query methods")
    class QueryMethods {

        @Test
        @DisplayName("isPositive returns true for positive quantity")
        void should_BePositive_When_AmountIsPositive() {
            var q = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
            assertThat(q.isPositive()).isTrue();
            assertThat(q.isZero()).isFalse();
        }
    }

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

        @Test
        @DisplayName("should be equal when same amount and UoM")
        void should_BeEqual_When_SameValues() {
            var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
            var q2 = Quantity.of(new BigDecimal("10.000000"), KILOGRAM).unsafeGetValue();

            assertThat(q1).isEqualTo(q2);
            assertThat(q1.hashCode()).isEqualTo(q2.hashCode());
        }

        @Test
        @DisplayName("should not be equal when different amount")
        void should_NotBeEqual_When_DifferentAmount() {
            var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
            var q2 = Quantity.of(new BigDecimal("20"), KILOGRAM).unsafeGetValue();

            assertThat(q1).isNotEqualTo(q2);
        }

        @Test
        @DisplayName("should not be equal when different UoM")
        void should_NotBeEqual_When_DifferentUom() {
            var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
            var q2 = Quantity.of(new BigDecimal("10"), GRAM).unsafeGetValue();

            assertThat(q1).isNotEqualTo(q2);
        }
    }

    @Nested
    @DisplayName("toString()")
    class ToStringTest {

        @Test
        @DisplayName("should format simple quantity")
        void should_FormatSimple() {
            var q = Quantity.of(new BigDecimal("10.5"), KILOGRAM).unsafeGetValue();
            assertThat(q.toString()).contains("10.5").contains("kg");
        }

        @Test
        @DisplayName("should format dual quantity")
        void should_FormatDual() {
            var q = Quantity.dual(new BigDecimal("10"), PIECE, new BigDecimal("23"), KILOGRAM).unsafeGetValue();
            assertThat(q.toString()).contains("10").contains("pc").contains("23").contains("kg");
        }
    }
}
