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

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.Test;

// Pruebas públicas de P4 para SalesReport. No las modifiquen.
class P4ReportsPublicTest {

    private static final LocalDate NOV_2 = LocalDate.of(2026, 11, 2);
    private static final LocalDate NOV_3 = LocalDate.of(2026, 11, 3);
    private static final LocalDate NOV_5 = LocalDate.of(2026, 11, 5);

    private static SaleLine milk(int q, long subtotal) {
        return new SaleLine("LEC-01", "Leche evaporada", "Lácteos", q, subtotal, 0);
    }

    private static SaleLine soda(int q, long subtotal, long discount) {
        return new SaleLine("GAS-01", "Gaseosa 500 ml", "Bebidas", q, subtotal, discount);
    }

    private static SaleLine rice(int grams, long subtotal, long discount) {
        return new SaleLine("ARR-01", "Arroz a granel", "Abarrotes", grams, subtotal, discount);
    }

    private static final SaleLine BREAD = new SaleLine("PAN-01", "Pan francés", "Panadería", 10, 300, 0);

    private final Sale s1 = new Sale("B-000001", NOV_2, "Rosa Quispe", List.of(milk(3, 1_260), soda(3, 750, 250)));
    private final Sale s2 = new Sale("B-000002", NOV_2, "", List.of(rice(2_000, 840, 0)));
    private final Sale s3 = new Sale("B-000003", NOV_3, "Jorge Huamán", List.of(milk(2, 840), BREAD));
    private final Sale s4 = new Sale("B-000004", NOV_5, "Rosa Quispe", List.of(soda(6, 1_500, 500), rice(1_000, 420, 42)));

    private final SalesReport report = new SalesReport(List.of(s1, s2, s3, s4));
    private final SalesReport empty = new SalesReport(List.of());

    @Test
    void countsAndAddsUpTheSales() {
        assertEquals(4, report.count());
        assertEquals(5_118, report.totalCents());
        assertEquals(0, empty.count());
        assertEquals(0, empty.totalCents());
    }

    @Test
    void totalByDayIsSortedByDate() {
        Map<LocalDate, Long> byDay = report.totalByDay();

        assertEquals(List.of(NOV_2, NOV_3, NOV_5), new ArrayList<>(byDay.keySet()));
        assertEquals(2_600, byDay.get(NOV_2));
        assertEquals(1_140, byDay.get(NOV_3));
        assertEquals(1_378, byDay.get(NOV_5));
    }

    @Test
    void totalByCategoryIsSortedByNameAndUsesTheDiscountedTotals() {
        Map<String, Long> byCategory = report.totalByCategory();

        assertEquals(List.of("Abarrotes", "Bebidas", "Lácteos", "Panadería"), new ArrayList<>(byCategory.keySet()));
        assertEquals(1_218, byCategory.get("Abarrotes"));
        assertEquals(1_500, byCategory.get("Bebidas"));
        assertEquals(2_100, byCategory.get("Lácteos"));
        assertEquals(300, byCategory.get("Panadería"));
    }

    @Test
    void topProductsAreTheOnesThatBroughtTheMostMoney() {
        assertEquals(List.of(
                new ProductTotal("LEC-01", "Leche evaporada", 2_100),
                new ProductTotal("GAS-01", "Gaseosa 500 ml", 1_500)), report.topProducts(2));
        assertEquals(4, report.topProducts(10).size());
        assertEquals(List.of(), report.topProducts(0));
        assertThrows(IllegalArgumentException.class, () -> report.topProducts(-1));
    }

    @Test
    void topProductsBreakTiesByCode() {
        SalesReport tie = new SalesReport(List.of(
                new Sale("B-000001", NOV_2, "", List.of(soda(4, 1_000, 0), BREAD)),
                new Sale("B-000002", NOV_2, "", List.of(new SaleLine("AZU-01", "Azúcar rubia", "Abarrotes", 2_000, 1_000, 0)))));

        assertEquals(List.of("AZU-01", "GAS-01", "PAN-01"),
                tie.topProducts(3).stream().map(ProductTotal::code).toList());
    }

    @Test
    void quantitySoldAddsUpUnitsOrGrams() {
        assertEquals(5, report.quantitySold("LEC-01"));
        assertEquals(3_000, report.quantitySold("ARR-01"));
        assertEquals(0, report.quantitySold("YOG-01"));
    }

    @Test
    void theLargestSaleMayNotExist() {
        assertEquals(Optional.of(s1), report.largestSale());
        assertEquals(Optional.empty(), empty.largestSale());
    }

    @Test
    void aTieForTheLargestSaleKeepsTheFirstOne() {
        Sale a = new Sale("B-000001", NOV_2, "", List.of(milk(1, 420)));
        Sale b = new Sale("B-000002", NOV_2, "", List.of(milk(1, 420)));

        assertEquals("B-000001", new SalesReport(List.of(a, b)).largestSale().orElseThrow().code());
    }

    @Test
    void frequentCustomersAreSortedAndIgnoreAnonymousSales() {
        assertEquals(Set.of("Rosa Quispe"), report.frequentCustomers(2));
        assertEquals(List.of("Jorge Huamán", "Rosa Quispe"), new ArrayList<>(report.frequentCustomers(1)));
        assertEquals(Set.of(), report.frequentCustomers(3));
        assertThrows(IllegalArgumentException.class, () -> report.frequentCustomers(0));
    }

    @Test
    void betweenKeepsTheSalesOfThoseDaysIncludingBothEnds() {
        SalesReport twoDays = report.between(NOV_2, NOV_3);

        assertEquals(3, twoDays.count());
        assertEquals(3_740, twoDays.totalCents());
        assertEquals(0, report.between(LocalDate.of(2026, 11, 4), LocalDate.of(2026, 11, 4)).count());
        assertThrows(IllegalArgumentException.class, () -> report.between(NOV_5, NOV_2));
    }

    @Test
    void anEmptyReportHasEmptyResults() {
        assertTrue(empty.totalByDay().isEmpty());
        assertTrue(empty.totalByCategory().isEmpty());
        assertEquals(List.of(), empty.topProducts(3));
        assertEquals(Set.of(), empty.frequentCustomers(1));
    }

    @Test
    void theReportKeepsItsOwnCopyOfTheSales() {
        List<Sale> sales = new ArrayList<>(List.of(s1, s2));
        SalesReport r = new SalesReport(sales);

        sales.add(s3);

        assertEquals(2, r.count());
    }
}
