package dev.lions.unionflow.server.api.dto.solidarite;

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

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

/**
 * Tests unitaires pour CreneauDisponibiliteDTO.
 * Couvre isValide, getDureeMinutes, isDisponibleLe (tous types), contientHeure, getLibelle,
 * et l'enum TypeCreneau.
 *
 * @author UnionFlow Team
 * @version 1.0
 * @since 2026-02-21
 */
@DisplayName("Tests CreneauDisponibiliteDTO")
class CreneauDisponibiliteDTOTest {

  @Nested
  @DisplayName("isValide")
  class IsValide {

    @Test
    @DisplayName("retourne false quand heureDebut est null")
    void testHeureDebutNull() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureFin(LocalTime.of(18, 0)).build();
      assertThat(dto.isValide()).isFalse();
    }

    @Test
    @DisplayName("retourne false quand heureFin est null")
    void testHeureFinNull() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0)).build();
      assertThat(dto.isValide()).isFalse();
    }

    @Test
    @DisplayName("retourne false quand heureFin avant heureDebut")
    void testHeureFinAvantDebut() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(18, 0))
          .heureFin(LocalTime.of(9, 0)).build();
      assertThat(dto.isValide()).isFalse();
    }

    @Test
    @DisplayName("retourne false quand heureFin égale heureDebut")
    void testHeuresEgales() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(9, 0)).build();
      assertThat(dto.isValide()).isFalse();
    }

    @Test
    @DisplayName("retourne true quand heureFin après heureDebut")
    void testValide() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0)).build();
      assertThat(dto.isValide()).isTrue();
    }
  }

  @Nested
  @DisplayName("getDureeMinutes")
  class GetDureeMinutes {

    @Test
    @DisplayName("retourne 0 quand créneau invalide")
    void testInvalide() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(18, 0))
          .heureFin(LocalTime.of(9, 0)).build();
      assertThat(dto.getDureeMinutes()).isEqualTo(0);
    }

    @Test
    @DisplayName("retourne la durée en minutes")
    void testDuree() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 30)).build();
      assertThat(dto.getDureeMinutes()).isEqualTo(210);
    }
  }

  @Nested
  @DisplayName("isDisponibleLe")
  class IsDisponibleLe {

    @Test
    @DisplayName("retourne false quand estActif est false")
    void testNonActif() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0))
          .estActif(false)
          .type(CreneauDisponibiliteDTO.TypeCreneau.RECURRENT)
          .jourSemaine(DayOfWeek.MONDAY).build();
      assertThat(dto.isDisponibleLe(LocalDate.of(2026, 2, 2))).isFalse();
    }

    @Test
    @DisplayName("PONCTUEL: true seulement si dateSpecifique égale à la date")
    void testPonctuel() {
      LocalDate date = LocalDate.of(2026, 2, 15);
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0))
          .estActif(true)
          .type(CreneauDisponibiliteDTO.TypeCreneau.PONCTUEL)
          .dateSpecifique(date).build();
      assertThat(dto.isDisponibleLe(date)).isTrue();
      assertThat(dto.isDisponibleLe(date.plusDays(1))).isFalse();
      dto.setDateSpecifique(null);
      assertThat(dto.isDisponibleLe(date)).isFalse();
    }

    @Test
    @DisplayName("RECURRENT: true si jourSemaine correspond")
    void testRecurrent() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0))
          .estActif(true)
          .type(CreneauDisponibiliteDTO.TypeCreneau.RECURRENT)
          .jourSemaine(DayOfWeek.WEDNESDAY).build();
      LocalDate mercredi = LocalDate.of(2026, 2, 18);
      assertThat(dto.isDisponibleLe(mercredi)).isTrue();
      assertThat(dto.isDisponibleLe(mercredi.plusDays(1))).isFalse();
      dto.setJourSemaine(null);
      assertThat(dto.isDisponibleLe(mercredi)).isFalse();
    }

    @Test
    @DisplayName("URGENCE et FLEXIBLE: toujours true si actif")
    void testUrgenceFlexible() {
      CreneauDisponibiliteDTO urg = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0))
          .estActif(true)
          .type(CreneauDisponibiliteDTO.TypeCreneau.URGENCE).build();
      CreneauDisponibiliteDTO flex = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0))
          .estActif(true)
          .type(CreneauDisponibiliteDTO.TypeCreneau.FLEXIBLE).build();
      LocalDate any = LocalDate.of(2026, 2, 20);
      assertThat(urg.isDisponibleLe(any)).isTrue();
      assertThat(flex.isDisponibleLe(any)).isTrue();
    }
  }

  @Nested
  @DisplayName("contientHeure")
  class ContientHeure {

    @Test
    @DisplayName("retourne false quand créneau invalide")
    void testInvalide() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(18, 0))
          .heureFin(LocalTime.of(9, 0)).build();
      assertThat(dto.contientHeure(LocalTime.of(12, 0))).isFalse();
    }

    @Test
    @DisplayName("retourne true quand heure dans le créneau")
    void testDedans() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0)).build();
      assertThat(dto.contientHeure(LocalTime.of(10, 30))).isTrue();
      assertThat(dto.contientHeure(LocalTime.of(9, 0))).isTrue();
      assertThat(dto.contientHeure(LocalTime.of(12, 0))).isTrue();
    }

    @Test
    @DisplayName("retourne false quand heure en dehors")
    void testDehors() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0)).build();
      assertThat(dto.contientHeure(LocalTime.of(8, 59))).isFalse();
      assertThat(dto.contientHeure(LocalTime.of(12, 1))).isFalse();
    }
  }

  @Nested
  @DisplayName("getLibelle")
  class GetLibelle {

    @Test
    @DisplayName("RECURRENT avec jourSemaine inclut le jour")
    void testRecurrent() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0))
          .type(CreneauDisponibiliteDTO.TypeCreneau.RECURRENT)
          .jourSemaine(DayOfWeek.MONDAY).build();
      assertThat(dto.getLibelle()).startsWith("MONDAY ");
      assertThat(dto.getLibelle()).endsWith("09:00 - 12:00");
    }

    @Test
    @DisplayName("RECURRENT sans jourSemaine: seulement heures")
    void testRecurrentSansJourSemaine() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0))
          .type(CreneauDisponibiliteDTO.TypeCreneau.RECURRENT)
          .jourSemaine(null).build();
      assertThat(dto.getLibelle()).isEqualTo("09:00 - 12:00");
    }

    @Test
    @DisplayName("PONCTUEL avec dateSpecifique inclut la date")
    void testPonctuel() {
      LocalDate date = LocalDate.of(2026, 2, 15);
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0))
          .type(CreneauDisponibiliteDTO.TypeCreneau.PONCTUEL)
          .dateSpecifique(date).build();
      assertThat(dto.getLibelle()).startsWith("2026-02-15 ");
      assertThat(dto.getLibelle()).endsWith("09:00 - 12:00");
    }

    @Test
    @DisplayName("PONCTUEL sans dateSpecifique: seulement heures")
    void testPonctuelSansDate() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0))
          .type(CreneauDisponibiliteDTO.TypeCreneau.PONCTUEL)
          .dateSpecifique(null).build();
      assertThat(dto.getLibelle()).isEqualTo("09:00 - 12:00");
    }

    @Test
    @DisplayName("autre type: seulement heures (URGENCE)")
    void testAutre() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(9, 0))
          .heureFin(LocalTime.of(12, 0))
          .type(CreneauDisponibiliteDTO.TypeCreneau.URGENCE).build();
      assertThat(dto.getLibelle()).isEqualTo("09:00 - 12:00");
    }

    @Test
    @DisplayName("FLEXIBLE: seulement heures (pas RECURRENT ni PONCTUEL)")
    void testFlexible() {
      CreneauDisponibiliteDTO dto = CreneauDisponibiliteDTO.builder()
          .heureDebut(LocalTime.of(14, 0))
          .heureFin(LocalTime.of(17, 0))
          .type(CreneauDisponibiliteDTO.TypeCreneau.FLEXIBLE).build();
      assertThat(dto.getLibelle()).isEqualTo("14:00 - 17:00");
    }
  }

  @Nested
  @DisplayName("TypeCreneau enum")
  class TypeCreneauEnum {

    @Test
    @DisplayName("values contient RECURRENT, PONCTUEL, URGENCE, FLEXIBLE")
    void testValues() {
      CreneauDisponibiliteDTO.TypeCreneau[] values = CreneauDisponibiliteDTO.TypeCreneau.values();
      assertThat(values).hasSize(4);
      assertThat(values).contains(
          CreneauDisponibiliteDTO.TypeCreneau.RECURRENT,
          CreneauDisponibiliteDTO.TypeCreneau.PONCTUEL,
          CreneauDisponibiliteDTO.TypeCreneau.URGENCE,
          CreneauDisponibiliteDTO.TypeCreneau.FLEXIBLE);
    }

    @ParameterizedTest
    @EnumSource(CreneauDisponibiliteDTO.TypeCreneau.class)
    @DisplayName("getLibelle non vide pour chaque type")
    void testGetLibelle(CreneauDisponibiliteDTO.TypeCreneau type) {
      assertThat(type.getLibelle()).isNotBlank();
    }

    @Test
    @DisplayName("valueOf pour chaque constante")
    void testValueOf() {
      assertThat(CreneauDisponibiliteDTO.TypeCreneau.valueOf("RECURRENT")).isEqualTo(CreneauDisponibiliteDTO.TypeCreneau.RECURRENT);
      assertThat(CreneauDisponibiliteDTO.TypeCreneau.valueOf("PONCTUEL")).isEqualTo(CreneauDisponibiliteDTO.TypeCreneau.PONCTUEL);
      assertThat(CreneauDisponibiliteDTO.TypeCreneau.valueOf("URGENCE")).isEqualTo(CreneauDisponibiliteDTO.TypeCreneau.URGENCE);
      assertThat(CreneauDisponibiliteDTO.TypeCreneau.valueOf("FLEXIBLE")).isEqualTo(CreneauDisponibiliteDTO.TypeCreneau.FLEXIBLE);
    }

    @Test
    @DisplayName("valueOf INEXISTANT lance IllegalArgumentException")
    void testValueOfInexistant() {
      assertThat(org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
          () -> CreneauDisponibiliteDTO.TypeCreneau.valueOf("INEXISTANT"))).isNotNull();
    }
  }
}
