package dev.lions.unionflow.server.resource;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;

import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;
import org.junit.jupiter.api.*;

/**
 * Tests d'intégration pour EvenementResource
 *
 * @author UnionFlow Team
 * @version 1.0
 * @since 2025-01-19
 */
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class EvenementResourceTest {

  private static final String BASE_PATH = "/api/evenements";

  @Inject
  EvenementRepository evenementRepository;
  @Inject
  OrganisationRepository organisationRepository;

  private Evenement testEvenement;
  private Organisation testOrganisation;

  @BeforeEach
  @Transactional
  void setupTestData() {
    // Créer une organisation de test
    testOrganisation = Organisation.builder()
        .nom("Organisation Test Événements")
        .typeOrganisation("ASSOCIATION")
        .statut("ACTIVE")
        .email("org-events-" + System.currentTimeMillis() + "@test.com")
        .build();
    testOrganisation.setDateCreation(LocalDateTime.now());
    testOrganisation.setActif(true);
    organisationRepository.persist(testOrganisation);

    // Créer un événement de test
    testEvenement = Evenement.builder()
        .titre("Événement Test")
        .description("Description de l'événement de test")
        .dateDebut(LocalDateTime.now().plusDays(7))
        .dateFin(LocalDateTime.now().plusDays(7).plusHours(3))
        .lieu("Lieu Test")
        .typeEvenement("REUNION")
        .statut("PLANIFIE")
        .capaciteMax(50)
        .prix(BigDecimal.valueOf(5000))
        .organisation(testOrganisation)
        .build();
    testEvenement.setDateCreation(LocalDateTime.now());
    testEvenement.setActif(true);
    evenementRepository.persist(testEvenement);
  }

  @AfterEach
  @Transactional
  void cleanupTestData() {
    // Supprimer tous les événements liés à l'organisation avant de supprimer
    // l'organisation
    if (testOrganisation != null && testOrganisation.getId() != null) {
      // Supprimer tous les événements de cette organisation
      java.util.List<Evenement> evenements = evenementRepository.findByOrganisation(testOrganisation.getId());
      for (Evenement evt : evenements) {
        evenementRepository.delete(evt);
      }
    }

    // Supprimer l'événement de test s'il existe encore
    if (testEvenement != null && testEvenement.getId() != null) {
      Evenement evtToDelete = evenementRepository.findById(testEvenement.getId());
      if (evtToDelete != null) {
        evenementRepository.delete(evtToDelete);
      }
    }

    // Supprimer l'organisation après avoir supprimé tous ses événements
    if (testOrganisation != null && testOrganisation.getId() != null) {
      Organisation orgToDelete = organisationRepository.findById(testOrganisation.getId());
      if (orgToDelete != null) {
        organisationRepository.delete(orgToDelete);
      }
    }
  }

  @Test
  @Order(1)
  @DisplayName("GET /api/evenements/test doit retourner un statut de connectivité")
  void testConnectivity() {
    given()
        .when()
        .get(BASE_PATH + "/test")
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON)
        .body("status", equalTo("success"))
        .body("message", notNullValue());
  }

  @Test
  @Order(2)
  @DisplayName("GET /api/evenements/count doit retourner le nombre d'événements")
  void testCountEvenements() {
    given()
        .when()
        .get(BASE_PATH + "/count")
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON)
        .body("count", greaterThanOrEqualTo(0))
        .body("status", equalTo("success"));
  }

  @Test
  @Order(3)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements doit retourner la liste des événements")
  void testListerEvenements() {
    given()
        .queryParam("page", 0)
        .queryParam("size", 20)
        .when()
        .get(BASE_PATH)
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON)
        .body("$", notNullValue());
  }

  @Test
  @Order(4)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements/{id} doit retourner un événement existant")
  void testObtenirEvenement() {
    UUID eventId = testEvenement.getId();

    given()
        .pathParam("id", eventId)
        .when()
        .get(BASE_PATH + "/{id}")
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON)
        .body("id", equalTo(eventId.toString()))
        .body("titre", equalTo("Événement Test"));
  }

  @Test
  @Order(5)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements/{id} doit retourner 404 pour un ID inexistant")
  void testObtenirEvenementInexistant() {
    UUID fakeId = UUID.randomUUID();

    given()
        .pathParam("id", fakeId)
        .when()
        .get(BASE_PATH + "/{id}")
        .then()
        .statusCode(404);
  }

  @Test
  @Order(6)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("POST /api/evenements doit créer un nouvel événement")
  void testCreerEvenement() {
    String eventJson = """
        {
          "titre": "Nouvel Événement Test",
          "description": "Description du nouvel événement",
          "dateDebut": "%s",
          "dateFin": "%s",
          "lieu": "Nouveau Lieu",
          "typeEvenement": "REUNION",
          "statut": "PLANIFIE",
          "capaciteMax": 100,
          "prix": 10000,
          "organisationId": "%s"
        }
        """
        .formatted(
            LocalDateTime.now().plusDays(14).toString(),
            LocalDateTime.now().plusDays(14).plusHours(4).toString(),
            testOrganisation.getId().toString());

    UUID createdId = UUID.fromString(
        given()
            .contentType(ContentType.JSON)
            .body(eventJson)
            .when()
            .post(BASE_PATH)
            .then()
            .statusCode(201)
            .contentType(ContentType.JSON)
            .body("titre", equalTo("Nouvel Événement Test"))
            .body("id", notNullValue())
            .extract()
            .path("id"));

    // Nettoyer l'événement créé
    Evenement created = evenementRepository.findById(createdId);
    if (created != null) {
      evenementRepository.delete(created);
    }
  }

  @Test
  @Order(7)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("POST /api/evenements doit retourner 400 pour données invalides")
  void testCreerEvenementInvalide() {
    String invalidEventJson = """
        {
          "titre": "",
          "description": "Description",
          "dateDebut": "%s"
        }
        """
        .formatted(LocalDateTime.now().plusDays(1).toString());

    given()
        .contentType(ContentType.JSON)
        .body(invalidEventJson)
        .when()
        .post(BASE_PATH)
        .then()
        .statusCode(400);
  }

  @Test
  @Order(8)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("PUT /api/evenements/{id} doit mettre à jour un événement")
  void testModifierEvenement() {
    UUID eventId = testEvenement.getId();
    // Récupérer l'événement existant pour préserver l'organisation
    Evenement existing = evenementRepository.findById(eventId);
    LocalDateTime dateDebut = LocalDateTime.now().plusDays(10);
    LocalDateTime dateFin = dateDebut.plusDays(1);
    String updatedEventJson = """
        {
          "titre": "Événement Modifié",
          "description": "Description modifiée",
          "dateDebut": "%s",
          "dateFin": "%s",
          "lieu": "Lieu Modifié",
          "typeEvenement": "REUNION",
          "statut": "PLANIFIE",
          "capaciteMax": 75,
          "prix": 7500.00,
          "associationId": "%s",
          "actif": true,
          "visiblePublic": true,
          "inscriptionRequise": false
        }
        """
        .formatted(dateDebut.toString(), dateFin.toString(), testOrganisation.getId().toString());

    given()
        .contentType(ContentType.JSON)
        .pathParam("id", eventId)
        .body(updatedEventJson)
        .when()
        .put(BASE_PATH + "/{id}")
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON)
        .body("titre", equalTo("Événement Modifié"))
        .body("lieu", equalTo("Lieu Modifié"));
  }

  @Test
  @Order(9)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("PUT /api/evenements/{id} doit retourner 404 pour ID inexistant")
  void testModifierEvenementInexistant() {
    UUID fakeId = UUID.randomUUID();
    LocalDateTime dateDebut = LocalDateTime.now().plusDays(1);
    String updatedEventJson = """
        {
          "titre": "Événement Test",
          "description": "Description test",
          "dateDebut": "%s",
          "lieu": "Lieu Test",
          "typeEvenement": "REUNION",
          "statut": "PLANIFIE",
          "associationId": "%s",
          "actif": true,
          "visiblePublic": true,
          "inscriptionRequise": false
        }
        """
        .formatted(dateDebut.toString(), testOrganisation.getId().toString());

    given()
        .contentType(ContentType.JSON)
        .pathParam("id", fakeId)
        .body(updatedEventJson)
        .when()
        .put(BASE_PATH + "/{id}")
        .then()
        .statusCode(404);
  }

  @Test
  @Order(10)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("DELETE /api/evenements/{id} doit supprimer un événement")
  void testSupprimerEvenement() {
    // Créer un événement temporaire pour la suppression
    Evenement tempEvent = Evenement.builder()
        .titre("Événement à Supprimer")
        .description("Description")
        .dateDebut(LocalDateTime.now().plusDays(5))
        .typeEvenement("REUNION")
        .statut("PLANIFIE")
        .organisation(testOrganisation)
        .build();
    tempEvent.setDateCreation(LocalDateTime.now());
    tempEvent.setActif(true);
    evenementRepository.persist(tempEvent);

    UUID tempId = tempEvent.getId();

    given()
        .pathParam("id", tempId)
        .when()
        .delete(BASE_PATH + "/{id}")
        .then()
        .statusCode(204);

    // Vérifier que l'événement a été supprimé (suppression logique : actif = false)
    Evenement deleted = evenementRepository.findById(tempId);
    assert deleted != null : "L'événement devrait toujours exister dans la base";
    assert !deleted.getActif() : "L'événement devrait être désactivé (actif = false)";
  }

  @Test
  @Order(11)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("DELETE /api/evenements/{id} doit retourner 404 pour ID inexistant")
  void testSupprimerEvenementInexistant() {
    UUID fakeId = UUID.randomUUID();

    given()
        .pathParam("id", fakeId)
        .when()
        .delete(BASE_PATH + "/{id}")
        .then()
        .statusCode(404);
  }

  @Test
  @Order(12)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements doit supporter la pagination")
  void testPagination() {
    given()
        .queryParam("page", 0)
        .queryParam("size", 5)
        .when()
        .get(BASE_PATH)
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON);
  }

  @Test
  @Order(13)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements doit supporter le tri")
  void testTri() {
    given()
        .queryParam("page", 0)
        .queryParam("size", 10)
        .queryParam("sort", "dateDebut")
        .queryParam("direction", "desc")
        .when()
        .get(BASE_PATH)
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON);
  }

  @Test
  @Order(14)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements/a-venir doit retourner les événements à venir")
  void testEvenementsAVenir() {
    given()
        .queryParam("page", 0)
        .queryParam("size", 10)
        .when()
        .get(BASE_PATH + "/a-venir")
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON);
  }

  @Test
  @Order(15)
  @DisplayName("GET /api/evenements/publics doit retourner les événements publics (sans auth)")
  void testEvenementsPublics() {
    given()
        .queryParam("page", 0)
        .queryParam("size", 20)
        .when()
        .get(BASE_PATH + "/publics")
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON);
  }

  @Test
  @Order(16)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements/recherche avec terme valide retourne 200")
  void testRechercherEvenements_avecTerme() {
    given()
        .queryParam("q", "Test")
        .queryParam("page", 0)
        .queryParam("size", 20)
        .when()
        .get(BASE_PATH + "/recherche")
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON);
  }

  @Test
  @Order(17)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements/recherche sans terme retourne 400")
  void testRechercherEvenements_sansTerme() {
    given()
        .queryParam("page", 0)
        .queryParam("size", 20)
        .when()
        .get(BASE_PATH + "/recherche")
        .then()
        .statusCode(400)
        .body("error", notNullValue());
  }

  @Test
  @Order(18)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements/type/{type} doit retourner les événements par type")
  void testEvenementsParType() {
    given()
        .pathParam("type", "REUNION")
        .queryParam("page", 0)
        .queryParam("size", 20)
        .when()
        .get(BASE_PATH + "/type/{type}")
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON);
  }

  @Test
  @Order(19)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("PATCH /api/evenements/{id}/statut sans statut retourne 400")
  void testChangerStatut_sansStatut() {
    UUID eventId = testEvenement.getId();

    given()
        .pathParam("id", eventId)
        .when()
        .patch(BASE_PATH + "/{id}/statut")
        .then()
        .statusCode(400)
        .body("error", notNullValue());
  }

  @Test
  @Order(20)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("PATCH /api/evenements/{id}/statut avec statut valide retourne résultat")
  void testChangerStatut_avecStatut() {
    UUID eventId = testEvenement.getId();

    given()
        .contentType(ContentType.JSON)
        .pathParam("id", eventId)
        .queryParam("statut", "ANNULE")
        .when()
        .patch(BASE_PATH + "/{id}/statut")
        .then()
        .statusCode(anyOf(equalTo(200), equalTo(400), equalTo(500)));
  }

  @Test
  @Order(21)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements/statistiques doit retourner les statistiques")
  void testObtenirStatistiques() {
    given()
        .when()
        .get(BASE_PATH + "/statistiques")
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON);
  }

  @Test
  @Order(22)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements/{id}/me/inscrit doit retourner le statut d'inscription")
  void testMeInscrit() {
    UUID eventId = testEvenement.getId();

    given()
        .pathParam("id", eventId)
        .when()
        .get(BASE_PATH + "/{id}/me/inscrit")
        .then()
        .statusCode(200)
        .body("inscrit", notNullValue());
  }

  @Test
  @Order(23)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements/{id}/participants doit retourner la liste des participants")
  void testGetParticipants() {
    UUID eventId = testEvenement.getId();

    given()
        .pathParam("id", eventId)
        .when()
        .get(BASE_PATH + "/{id}/participants")
        .then()
        .statusCode(200)
        .contentType(ContentType.JSON);
  }

  @Test
  @Order(24)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("GET /api/evenements/mes-inscriptions doit retourner mes inscriptions ou erreur auth")
  void testGetMesInscriptions() {
    given()
        .when()
        .get(BASE_PATH + "/mes-inscriptions")
        .then()
        // L'endpoint appelle keycloakService.getCurrentUserEmail() → SecurityIdentity anonyme en
        // test service-layer → IllegalStateException → exception mapper → 400
        .statusCode(anyOf(equalTo(200), equalTo(400), equalTo(500)));
  }

  @Test
  @Order(25)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("POST /api/evenements/{id}/feedback avec note invalide retourne 400")
  void testSoumetteFeedback_noteInvalide() {
    UUID eventId = testEvenement.getId();

    given()
        .contentType(ContentType.JSON)
        .pathParam("id", eventId)
        .body(java.util.Map.of("note", 6, "commentaire", "Super événement!"))
        .when()
        .post(BASE_PATH + "/{id}/feedback")
        .then()
        .statusCode(400)
        .body("error", notNullValue());
  }

  @Test
  @Order(26)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("POST /api/evenements/{id}/feedback avec note=0 retourne 400")
  void testSoumetteFeedback_noteZero() {
    UUID eventId = testEvenement.getId();

    given()
        .contentType(ContentType.JSON)
        .pathParam("id", eventId)
        .body(java.util.Map.of("note", 0, "commentaire", "Note nulle"))
        .when()
        .post(BASE_PATH + "/{id}/feedback")
        .then()
        .statusCode(400)
        .body("error", notNullValue());
  }

  @Test
  @Order(27)
  @DisplayName("GET /api/evenements/{id}/feedbacks doit retourner les feedbacks ou erreur")
  void testGetFeedbacks() {
    UUID eventId = testEvenement.getId();

    // If noteMoyenne is null (no feedbacks), Map.of() will throw NPE → 500
    given()
        .pathParam("id", eventId)
        .when()
        .get(BASE_PATH + "/{id}/feedbacks")
        .then()
        .statusCode(anyOf(equalTo(200), equalTo(500)));
  }

  @Test
  @Order(28)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("POST /api/evenements/{id}/inscriptions avec événement existant retourne résultat")
  void testInscrireEvenement() {
    UUID eventId = testEvenement.getId();

    given()
        .contentType(ContentType.JSON)
        .pathParam("id", eventId)
        .when()
        .post(BASE_PATH + "/{id}/inscriptions")
        .then()
        // Peut retourner 201 (inscription créée) ou 400 (déjà inscrit)
        .statusCode(anyOf(equalTo(201), equalTo(400)));
  }

  @Test
  @Order(29)
  @TestSecurity(user = "admin@unionflow.com", roles = { "ADMIN" })
  @DisplayName("DELETE /api/evenements/{id}/inscriptions avec événement non inscrit retourne résultat")
  void testDesinscrireEvenement() {
    UUID eventId = testEvenement.getId();

    given()
        .pathParam("id", eventId)
        .when()
        .delete(BASE_PATH + "/{id}/inscriptions")
        // Peut retourner 204 (désinscription), 404 (pas inscrit), 400 ou 500 (auth échoue en test)
        .then()
        .statusCode(anyOf(equalTo(204), equalTo(404), equalTo(400), equalTo(500)));
  }
}
