import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.Test;

// Pruebas públicas de P2 para los descuentos y las promociones. No las modifiquen.
class P2DiscountsPublicTest {

    private static final LocalDate NOV_1 = LocalDate.of(2026, 11, 1);

    private final Product milk = new Product("LEC-01", "Leche evaporada", 420, 24, "Lácteos");
    private final Product soda = new Product("GAS-01", "Gaseosa 500 ml", 250, 30, "Bebidas");
    private final BulkProduct rice = new BulkProduct("ARR-01", "Arroz a granel", 420, 25_000, "Abarrotes");

    private static PerishableProduct yogurtExpiringOn(LocalDate date) {
        return new PerishableProduct("YOG-01", "Yogur de fresa 1 l", 690, 6, "Lácteos", date);
    }

    /** Test double: un descuento que siempre devuelve el mismo monto. */
    static class FixedDiscount implements Discount {
        private final long cents;

        FixedDiscount(long cents) {
            this.cents = cents;
        }

        @Override
        public long discountCents(Product product, int quantity) {
            return cents;
        }
    }

    /** Un porcentaje que se aplica a todo: para probar la clase abstracta. */
    static class EverythingOff extends PercentageDiscount {
        EverythingOff(int percent) {
            super(percent);
        }

        @Override
        protected boolean appliesTo(Product product) {
            return true;
        }
    }

    @Test
    void aCategoryDiscountAppliesOnlyToItsCategory() {
        Discount d = new CategoryDiscount("Lácteos", 10);

        assertEquals(126, d.discountCents(milk, 3));
        assertEquals(0, d.discountCents(soda, 3));
    }

    @Test
    void aPercentageIsRoundedToTheNearestCentWithHalvesUp() {
        Product candy = new Product("CAR-01", "Caramelo", 245, 100, "Golosinas");

        assertEquals(25, new CategoryDiscount("Golosinas", 10).discountCents(candy, 1));   // 24.5
        assertEquals(24, new CategoryDiscount("Golosinas", 10).discountCents(new Product("CAR-02", "Chicle", 244, 1, "Golosinas"), 1));
    }

    @Test
    void aPercentageDiscountWorksOnBulkProducts() {
        // 1.5 kg de arroz cuestan S/ 6.30; el 10 % es S/ 0.63.
        assertEquals(63, new CategoryDiscount("Abarrotes", 10).discountCents(rice, 1_500));
    }

    @Test
    void thePercentageMustBeBetweenOneAndAHundred() {
        assertThrows(IllegalArgumentException.class, () -> new CategoryDiscount("Lácteos", 0));
        assertThrows(IllegalArgumentException.class, () -> new CategoryDiscount("Lácteos", 101));
        assertThrows(IllegalArgumentException.class, () -> new EverythingOff(-5));
    }

    @Test
    void theAbstractClassComputesThePercentage() {
        PercentageDiscount all = new EverythingOff(100);

        assertEquals(100, all.percent());
        assertEquals(1_260, all.discountCents(milk, 3));
    }

    @Test
    void aNearExpiryDiscountAppliesWithinTheGivenDays() {
        Discount d = new NearExpiryDiscount(NOV_1, 3, 30);

        assertEquals(207, d.discountCents(yogurtExpiringOn(NOV_1), 1));               // vence hoy
        assertEquals(207, d.discountCents(yogurtExpiringOn(NOV_1.plusDays(3)), 1));   // en 3 días
        assertEquals(0, d.discountCents(yogurtExpiringOn(NOV_1.plusDays(4)), 1));     // en 4 días
    }

    @Test
    void aNearExpiryDiscountIgnoresExpiredAndNonPerishableProducts() {
        Discount d = new NearExpiryDiscount(NOV_1, 3, 30);

        assertEquals(0, d.discountCents(yogurtExpiringOn(NOV_1.minusDays(1)), 1));
        assertEquals(0, d.discountCents(milk, 1));
        assertThrows(IllegalArgumentException.class, () -> new NearExpiryDiscount(NOV_1, -1, 30));
    }

    @Test
    void takeThreePayTwo() {
        Discount d = new MultiBuyDiscount("GAS-01", 3, 2);

        assertEquals(0, d.discountCents(soda, 2));
        assertEquals(250, d.discountCents(soda, 3));
        assertEquals(500, d.discountCents(soda, 7));
        assertEquals(0, d.discountCents(milk, 3));
    }

    @Test
    void aMultiBuyDiscountDoesNotApplyToBulkProducts() {
        Discount d = new MultiBuyDiscount("ARR-01", 3, 2);

        assertEquals(0, d.discountCents(rice, 3_000));
    }

    @Test
    void aMultiBuyNeedsTakeGreaterThanPay() {
        assertThrows(IllegalArgumentException.class, () -> new MultiBuyDiscount("GAS-01", 2, 2));
        assertThrows(IllegalArgumentException.class, () -> new MultiBuyDiscount("GAS-01", 3, 0));
    }

    @Test
    void withoutPromotionsThePriceIsTheBasePrice() {
        Promotions promos = new Promotions();

        assertEquals(0, promos.discountFor(rice, 1_500));
        assertEquals(630, promos.priceFor(rice, 1_500));
    }

    @Test
    void discountsAreNotAddedUpTheBiggestOneWins() {
        Promotions promos = new Promotions();
        promos.add(new CategoryDiscount("Bebidas", 10));
        promos.add(new MultiBuyDiscount("GAS-01", 3, 2));
        promos.add(new FixedDiscount(40));

        assertEquals(250, promos.discountFor(soda, 3));    // 10 %: 75, 3x2: 250, fijo: 40
        assertEquals(500, promos.priceFor(soda, 3));
        assertEquals(50, promos.discountFor(soda, 2));     // 10 %: 50, 3x2: 0, fijo: 40
    }

    @Test
    void theDiscountNeverExceedsThePrice() {
        Promotions promos = new Promotions();
        promos.add(new FixedDiscount(10_000));

        assertEquals(420, promos.discountFor(milk, 1));
        assertEquals(0, promos.priceFor(milk, 1));
    }

    @Test
    void discountsKeepsTheOrderAndCannotBeModified() {
        Promotions promos = new Promotions();
        Discount a = new FixedDiscount(1);
        Discount b = new FixedDiscount(2);
        promos.add(a);
        promos.add(b);

        List<Discount> list = promos.discounts();

        assertEquals(List.of(a, b), list);
        assertThrows(UnsupportedOperationException.class, () -> list.add(a));
        assertThrows(NullPointerException.class, () -> promos.add(null));
    }
}
