package dev.lions.unionflow.server.entity;

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

import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;

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

@DisplayName("Budget")
class BudgetTest {

    private static Organisation newOrganisation() {
        Organisation o = new Organisation();
        o.setId(UUID.randomUUID());
        return o;
    }

    private static BudgetLine newLine(BigDecimal planned, BigDecimal realized) {
        BudgetLine line = new BudgetLine();
        line.setAmountPlanned(planned);
        line.setAmountRealized(realized);
        return line;
    }

    // -------------------------------------------------------------------------
    // getRealizationRate
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("getRealizationRate: totalPlanned == 0 renvoie 0.0")
    void getRealizationRate_zeroPlan_returns0() {
        Budget b = new Budget();
        b.setTotalPlanned(BigDecimal.ZERO);
        b.setTotalRealized(new BigDecimal("500.00"));
        assertThat(b.getRealizationRate()).isEqualTo(0.0);
    }

    @Test
    @DisplayName("getRealizationRate: 500 réalisé / 1000 prévu = 50%")
    void getRealizationRate_withPlan_returnsRatio() {
        Budget b = new Budget();
        b.setTotalPlanned(new BigDecimal("1000.00"));
        b.setTotalRealized(new BigDecimal("500.00"));
        assertThat(b.getRealizationRate()).isEqualTo(50.0);
    }

    // -------------------------------------------------------------------------
    // getVariance
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("getVariance: renvoie réalisé - prévu")
    void getVariance_returnsRealized_minus_planned() {
        Budget b = new Budget();
        b.setTotalPlanned(new BigDecimal("1000.00"));
        b.setTotalRealized(new BigDecimal("1200.00"));
        assertThat(b.getVariance()).isEqualByComparingTo("200.00");
    }

    // -------------------------------------------------------------------------
    // isOverBudget
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("isOverBudget: réalisé > prévu renvoie true")
    void isOverBudget_whenOver_returnsTrue() {
        Budget b = new Budget();
        b.setTotalPlanned(new BigDecimal("1000.00"));
        b.setTotalRealized(new BigDecimal("1001.00"));
        assertThat(b.isOverBudget()).isTrue();
    }

    @Test
    @DisplayName("isOverBudget: réalisé == prévu renvoie false")
    void isOverBudget_whenEqual_returnsFalse() {
        Budget b = new Budget();
        b.setTotalPlanned(new BigDecimal("1000.00"));
        b.setTotalRealized(new BigDecimal("1000.00"));
        assertThat(b.isOverBudget()).isFalse();
    }

    // -------------------------------------------------------------------------
    // isActive
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("isActive: statut ACTIVE renvoie true")
    void isActive_active_returnsTrue() {
        Budget b = new Budget();
        b.setStatus("ACTIVE");
        assertThat(b.isActive()).isTrue();
    }

    @Test
    @DisplayName("isActive: statut DRAFT renvoie false")
    void isActive_draft_returnsFalse() {
        Budget b = new Budget();
        b.setStatus("DRAFT");
        assertThat(b.isActive()).isFalse();
    }

    // -------------------------------------------------------------------------
    // isCurrentPeriod
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("isCurrentPeriod: aujourd'hui dans la période renvoie true")
    void isCurrentPeriod_duringPeriod_returnsTrue() {
        Budget b = new Budget();
        b.setStartDate(LocalDate.now().minusDays(5));
        b.setEndDate(LocalDate.now().plusDays(5));
        assertThat(b.isCurrentPeriod()).isTrue();
    }

    @Test
    @DisplayName("isCurrentPeriod: période terminée renvoie false")
    void isCurrentPeriod_afterPeriod_returnsFalse() {
        Budget b = new Budget();
        b.setStartDate(LocalDate.now().minusDays(10));
        b.setEndDate(LocalDate.now().minusDays(1));
        assertThat(b.isCurrentPeriod()).isFalse();
    }

    @Test
    @DisplayName("isCurrentPeriod: période pas encore commencée renvoie false")
    void isCurrentPeriod_beforePeriod_returnsFalse() {
        Budget b = new Budget();
        b.setStartDate(LocalDate.now().plusDays(1));
        b.setEndDate(LocalDate.now().plusDays(10));
        assertThat(b.isCurrentPeriod()).isFalse();
    }

    // -------------------------------------------------------------------------
    // addLine / removeLine / recalculateTotals
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("addLine: ajoute la ligne, lie le budget parent et recalcule les totaux")
    void addLine_addsLineAndRecalculates() {
        Budget b = Budget.builder()
            .name("Budget Test")
            .organisation(newOrganisation())
            .period("ANNUAL")
            .year(2026)
            .status("DRAFT")
            .createdById(UUID.randomUUID())
            .createdAtBudget(LocalDateTime.now())
            .startDate(LocalDate.of(2026, 1, 1))
            .endDate(LocalDate.of(2026, 12, 31))
            .build();

        BudgetLine line = newLine(new BigDecimal("500.00"), new BigDecimal("200.00"));
        b.addLine(line);

        assertThat(b.getLines()).hasSize(1);
        assertThat(line.getBudget()).isSameAs(b);
        assertThat(b.getTotalPlanned()).isEqualByComparingTo("500.00");
        assertThat(b.getTotalRealized()).isEqualByComparingTo("200.00");
    }

    @Test
    @DisplayName("removeLine: retire la ligne, délie le budget parent et recalcule les totaux")
    void removeLine_removesAndRecalculates() {
        Budget b = Budget.builder()
            .name("Budget Test")
            .organisation(newOrganisation())
            .period("ANNUAL")
            .year(2026)
            .status("DRAFT")
            .createdById(UUID.randomUUID())
            .createdAtBudget(LocalDateTime.now())
            .startDate(LocalDate.of(2026, 1, 1))
            .endDate(LocalDate.of(2026, 12, 31))
            .build();

        BudgetLine line1 = newLine(new BigDecimal("300.00"), new BigDecimal("100.00"));
        BudgetLine line2 = newLine(new BigDecimal("200.00"), new BigDecimal("50.00"));
        b.addLine(line1);
        b.addLine(line2);
        assertThat(b.getLines()).hasSize(2);

        b.removeLine(line1);

        assertThat(b.getLines()).hasSize(1);
        assertThat(line1.getBudget()).isNull();
        assertThat(b.getTotalPlanned()).isEqualByComparingTo("200.00");
        assertThat(b.getTotalRealized()).isEqualByComparingTo("50.00");
    }

    @Test
    @DisplayName("recalculateTotals: somme correcte avec plusieurs lignes")
    void recalculateTotals_multipleLines_sumsCorrectly() {
        Budget b = Budget.builder()
            .name("Budget Multi")
            .organisation(newOrganisation())
            .period("ANNUAL")
            .year(2026)
            .status("DRAFT")
            .createdById(UUID.randomUUID())
            .createdAtBudget(LocalDateTime.now())
            .startDate(LocalDate.of(2026, 1, 1))
            .endDate(LocalDate.of(2026, 12, 31))
            .build();

        b.addLine(newLine(new BigDecimal("100.00"), new BigDecimal("80.00")));
        b.addLine(newLine(new BigDecimal("200.00"), new BigDecimal("150.00")));
        b.addLine(newLine(new BigDecimal("300.00"), new BigDecimal("320.00")));

        assertThat(b.getTotalPlanned()).isEqualByComparingTo("600.00");
        assertThat(b.getTotalRealized()).isEqualByComparingTo("550.00");
    }

    // -------------------------------------------------------------------------
    // onCreate (réflexion)
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("onCreate: initialise les champs null (createdAtBudget, currency, status, totalPlanned, totalRealized)")
    void onCreate_initializesNullFields() throws Exception {
        Budget b = new Budget();
        b.setCreatedAtBudget(null);
        b.setCurrency(null);
        b.setStatus(null);
        b.setTotalPlanned(null);
        b.setTotalRealized(null);

        Method onCreate = Budget.class.getDeclaredMethod("onCreate");
        onCreate.setAccessible(true);
        onCreate.invoke(b);

        assertThat(b.getCreatedAtBudget()).isNotNull();
        assertThat(b.getCurrency()).isEqualTo("XOF");
        assertThat(b.getStatus()).isEqualTo("DRAFT");
        assertThat(b.getTotalPlanned()).isEqualByComparingTo(BigDecimal.ZERO);
        assertThat(b.getTotalRealized()).isEqualByComparingTo(BigDecimal.ZERO);
    }

    @Test
    @DisplayName("onCreate: ne remplace pas les champs déjà renseignés")
    void onCreate_doesNotOverrideExistingFields() throws Exception {
        LocalDateTime existingDate = LocalDateTime.of(2025, 6, 15, 10, 0);
        Budget b = new Budget();
        b.setCreatedAtBudget(existingDate);
        b.setCurrency("EUR");
        b.setStatus("ACTIVE");
        b.setTotalPlanned(new BigDecimal("5000.00"));
        b.setTotalRealized(new BigDecimal("2500.00"));

        Method onCreate = Budget.class.getDeclaredMethod("onCreate");
        onCreate.setAccessible(true);
        onCreate.invoke(b);

        assertThat(b.getCreatedAtBudget()).isEqualTo(existingDate);
        assertThat(b.getCurrency()).isEqualTo("EUR");
        assertThat(b.getStatus()).isEqualTo("ACTIVE");
        assertThat(b.getTotalPlanned()).isEqualByComparingTo("5000.00");
        assertThat(b.getTotalRealized()).isEqualByComparingTo("2500.00");
    }

    // -------------------------------------------------------------------------
    // builder
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("builder: crée un objet complet avec tous les champs")
    void builder_createsComplete() {
        UUID createdById = UUID.randomUUID();
        UUID approvedById = UUID.randomUUID();
        Organisation org = newOrganisation();
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime approvedAt = now.plusDays(1);
        LocalDate start = LocalDate.of(2026, 1, 1);
        LocalDate end = LocalDate.of(2026, 12, 31);

        Budget b = Budget.builder()
            .name("Budget Annuel 2026")
            .description("Description du budget")
            .organisation(org)
            .period("ANNUAL")
            .year(2026)
            .month(null)
            .status("ACTIVE")
            .totalPlanned(new BigDecimal("1000000.00"))
            .totalRealized(new BigDecimal("500000.00"))
            .currency("XOF")
            .createdById(createdById)
            .createdAtBudget(now)
            .approvedAt(approvedAt)
            .approvedById(approvedById)
            .startDate(start)
            .endDate(end)
            .metadata("{\"note\":\"test\"}")
            .build();

        assertThat(b.getName()).isEqualTo("Budget Annuel 2026");
        assertThat(b.getDescription()).isEqualTo("Description du budget");
        assertThat(b.getOrganisation()).isSameAs(org);
        assertThat(b.getPeriod()).isEqualTo("ANNUAL");
        assertThat(b.getYear()).isEqualTo(2026);
        assertThat(b.getMonth()).isNull();
        assertThat(b.getStatus()).isEqualTo("ACTIVE");
        assertThat(b.getTotalPlanned()).isEqualByComparingTo("1000000.00");
        assertThat(b.getTotalRealized()).isEqualByComparingTo("500000.00");
        assertThat(b.getCurrency()).isEqualTo("XOF");
        assertThat(b.getCreatedById()).isEqualTo(createdById);
        assertThat(b.getCreatedAtBudget()).isEqualTo(now);
        assertThat(b.getApprovedAt()).isEqualTo(approvedAt);
        assertThat(b.getApprovedById()).isEqualTo(approvedById);
        assertThat(b.getStartDate()).isEqualTo(start);
        assertThat(b.getEndDate()).isEqualTo(end);
        assertThat(b.getMetadata()).isEqualTo("{\"note\":\"test\"}");
        assertThat(b.getLines()).isNotNull().isEmpty();
    }
}
