package dev.lions.unionflow.server.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import dev.lions.unionflow.server.api.dto.finance_workflow.request.ApproveTransactionRequest;
import dev.lions.unionflow.server.api.dto.finance_workflow.request.RejectTransactionRequest;
import dev.lions.unionflow.server.api.dto.finance_workflow.response.TransactionApprovalResponse;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.TransactionApproval;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.repository.TransactionApprovalRepository;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.NotFoundException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@QuarkusTest
class ApprovalServiceTest {

    @Inject
    ApprovalService approvalService;

    @InjectMock
    TransactionApprovalRepository approvalRepository;

    @InjectMock
    MembreRepository membreRepository;

    @InjectMock
    OrganisationRepository organisationRepository;

    @InjectMock
    JsonWebToken jwt;

    private static final String USER_EMAIL = "approver@unionflow.test";
    private static final UUID USER_ID = UUID.randomUUID();
    private static final UUID ORG_ID = UUID.randomUUID();

    @BeforeEach
    void setup() {
        when(jwt.<String>getClaim("email")).thenReturn(USER_EMAIL);
        when(jwt.<String>getClaim("sub")).thenReturn(USER_ID.toString());
        when(jwt.<String>getClaim("role")).thenReturn("MANAGER");
    }

    // ─── helpers ────────────────────────────────────────────────────────────────

    private Membre buildMembre(UUID id, String email) {
        Membre m = new Membre();
        m.setId(id);
        m.setNom("Doe");
        m.setPrenom("John");
        m.setEmail(email);
        m.setNumeroMembre("MBR-" + id.toString().substring(0, 8));
        return m;
    }

    private TransactionApproval createMockApproval() {
        TransactionApproval approval = TransactionApproval.builder()
                .transactionId(UUID.randomUUID())
                .transactionType("CONTRIBUTION")
                .amount(BigDecimal.valueOf(500_000))
                .currency("XOF")
                .requesterId(UUID.randomUUID()) // ID différent de USER_ID
                .requesterName("Test Requester")
                .requiredLevel("LEVEL1")
                .status("PENDING")
                .createdAt(LocalDateTime.now())
                .expiresAt(LocalDateTime.now().plusDays(7))
                .build();
        approval.setId(UUID.randomUUID());
        return approval;
    }

    // ─── requestApproval ─────────────────────────────────────────────────────────

    @Test
    @DisplayName("requestApproval_memberNotFound_throwsForbidden")
    void requestApproval_memberNotFound_throwsForbidden() {
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> approvalService.requestApproval(
                UUID.randomUUID(), "CONTRIBUTION", 100_000.0, null))
                .isInstanceOf(ForbiddenException.class);
    }

    @Test
    @DisplayName("requestApproval_orgNotFound_throwsNotFound")
    void requestApproval_orgNotFound_throwsNotFound() {
        Membre membre = buildMembre(USER_ID, USER_EMAIL);
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(membre));
        when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> approvalService.requestApproval(
                UUID.randomUUID(), "CONTRIBUTION", 100_000.0, ORG_ID))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("requestApproval_noOrg_level3Amount_success")
    void requestApproval_noOrg_level3Amount_success() {
        Membre membre = buildMembre(USER_ID, USER_EMAIL);
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(membre));

        TransactionApprovalResponse result = approvalService.requestApproval(
                UUID.randomUUID(), "WITHDRAWAL", 6_000_000.0, null);

        assertThat(result).isNotNull();
        assertThat(result.getRequiredLevel()).isEqualTo("LEVEL3");
        assertThat(result.getOrganizationId()).isNull();
        verify(approvalRepository).persist(any(TransactionApproval.class));
    }

    @Test
    @DisplayName("requestApproval_level2Amount_success")
    void requestApproval_level2Amount_success() {
        Membre membre = buildMembre(USER_ID, USER_EMAIL);
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(membre));

        TransactionApprovalResponse result = approvalService.requestApproval(
                UUID.randomUUID(), "TRANSFER", 2_000_000.0, null);

        assertThat(result).isNotNull();
        assertThat(result.getRequiredLevel()).isEqualTo("LEVEL2");
        verify(approvalRepository).persist(any(TransactionApproval.class));
    }

    @Test
    @DisplayName("requestApproval_level1Amount_success")
    void requestApproval_level1Amount_success() {
        Membre membre = buildMembre(USER_ID, USER_EMAIL);
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(membre));

        TransactionApprovalResponse result = approvalService.requestApproval(
                UUID.randomUUID(), "DEPOSIT", 200_000.0, null);

        assertThat(result).isNotNull();
        assertThat(result.getRequiredLevel()).isEqualTo("LEVEL1");
        verify(approvalRepository).persist(any(TransactionApproval.class));
    }

    @Test
    @DisplayName("requestApproval_noneAmount_success")
    void requestApproval_noneAmount_success() {
        Membre membre = buildMembre(USER_ID, USER_EMAIL);
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(membre));

        TransactionApprovalResponse result = approvalService.requestApproval(
                UUID.randomUUID(), "CONTRIBUTION", 50_000.0, null);

        assertThat(result).isNotNull();
        assertThat(result.getRequiredLevel()).isEqualTo("NONE");
        verify(approvalRepository).persist(any(TransactionApproval.class));
    }

    // ─── getPendingApprovals ─────────────────────────────────────────────────────

    @Test
    @DisplayName("getPendingApprovals_withOrgId_callsByOrg")
    void getPendingApprovals_withOrgId_callsByOrg() {
        TransactionApproval approval = createMockApproval();
        when(approvalRepository.findPendingByOrganisation(ORG_ID)).thenReturn(List.of(approval));

        List<TransactionApprovalResponse> result = approvalService.getPendingApprovals(ORG_ID);

        assertThat(result).hasSize(1);
        verify(approvalRepository).findPendingByOrganisation(ORG_ID);
    }

    @Test
    @DisplayName("getPendingApprovals_withoutOrgId_callsAll")
    void getPendingApprovals_withoutOrgId_callsAll() {
        TransactionApproval approval = createMockApproval();
        when(approvalRepository.findPending()).thenReturn(List.of(approval));

        List<TransactionApprovalResponse> result = approvalService.getPendingApprovals(null);

        assertThat(result).hasSize(1);
        verify(approvalRepository).findPending();
    }

    // ─── getApprovalById ─────────────────────────────────────────────────────────

    @Test
    @DisplayName("getApprovalById_notFound_throwsNotFound")
    void getApprovalById_notFound_throwsNotFound() {
        when(approvalRepository.findByIdOptional(any())).thenReturn(Optional.empty());

        assertThatThrownBy(() -> approvalService.getApprovalById(UUID.randomUUID()))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("getApprovalById_found_returnsDto")
    void getApprovalById_found_returnsDto() {
        TransactionApproval approval = createMockApproval();
        UUID approvalId = approval.getId();
        when(approvalRepository.findByIdOptional(approvalId)).thenReturn(Optional.of(approval));

        TransactionApprovalResponse result = approvalService.getApprovalById(approvalId);

        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(approvalId);
        assertThat(result.getStatus()).isEqualTo("PENDING");
    }

    // ─── approveTransaction ──────────────────────────────────────────────────────

    @Test
    @DisplayName("approveTransaction_notFound_throwsNotFound")
    void approveTransaction_notFound_throwsNotFound() {
        when(approvalRepository.findByIdOptional(any())).thenReturn(Optional.empty());
        ApproveTransactionRequest req = new ApproveTransactionRequest("OK");

        assertThatThrownBy(() -> approvalService.approveTransaction(UUID.randomUUID(), req))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("approveTransaction_notPending_throwsForbidden")
    void approveTransaction_notPending_throwsForbidden() {
        TransactionApproval approval = createMockApproval();
        approval.setStatus("VALIDATED");
        UUID approvalId = approval.getId();
        when(approvalRepository.findByIdOptional(approvalId)).thenReturn(Optional.of(approval));
        ApproveTransactionRequest req = new ApproveTransactionRequest("OK");

        assertThatThrownBy(() -> approvalService.approveTransaction(approvalId, req))
                .isInstanceOf(ForbiddenException.class);
    }

    @Test
    @DisplayName("approveTransaction_expired_throwsForbidden")
    void approveTransaction_expired_throwsForbidden() {
        TransactionApproval approval = createMockApproval();
        // Forcer l'expiration en mettant expiresAt dans le passé
        approval.setExpiresAt(LocalDateTime.now().minusDays(1));
        UUID approvalId = approval.getId();
        when(approvalRepository.findByIdOptional(approvalId)).thenReturn(Optional.of(approval));
        ApproveTransactionRequest req = new ApproveTransactionRequest("OK");

        assertThatThrownBy(() -> approvalService.approveTransaction(approvalId, req))
                .isInstanceOf(ForbiddenException.class);

        // Vérifie que le statut a été mis à EXPIRED et persisté
        assertThat(approval.getStatus()).isEqualTo("EXPIRED");
        verify(approvalRepository).persist(approval);
    }

    @Test
    @DisplayName("approveTransaction_memberNotFound_throwsForbidden")
    void approveTransaction_memberNotFound_throwsForbidden() {
        TransactionApproval approval = createMockApproval();
        UUID approvalId = approval.getId();
        when(approvalRepository.findByIdOptional(approvalId)).thenReturn(Optional.of(approval));
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.empty());
        ApproveTransactionRequest req = new ApproveTransactionRequest("OK");

        assertThatThrownBy(() -> approvalService.approveTransaction(approvalId, req))
                .isInstanceOf(ForbiddenException.class);
    }

    @Test
    @DisplayName("approveTransaction_ownRequest_throwsForbidden")
    void approveTransaction_ownRequest_throwsForbidden() {
        TransactionApproval approval = createMockApproval();
        // Mettre le requesterId égal à USER_ID pour simuler la propre demande
        approval.setRequesterId(USER_ID);
        UUID approvalId = approval.getId();
        when(approvalRepository.findByIdOptional(approvalId)).thenReturn(Optional.of(approval));
        Membre membre = buildMembre(USER_ID, USER_EMAIL);
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(membre));
        ApproveTransactionRequest req = new ApproveTransactionRequest("OK");

        assertThatThrownBy(() -> approvalService.approveTransaction(approvalId, req))
                .isInstanceOf(ForbiddenException.class);
    }

    @Test
    @DisplayName("approveTransaction_allApprovals_validatesTransaction")
    void approveTransaction_allApprovals_validatesTransaction() {
        // LEVEL1 → 1 approbation requise, aucune existante → après approbation = VALIDATED
        TransactionApproval approval = createMockApproval();
        approval.setRequiredLevel("LEVEL1");
        UUID approvalId = approval.getId();
        when(approvalRepository.findByIdOptional(approvalId)).thenReturn(Optional.of(approval));
        Membre membre = buildMembre(USER_ID, USER_EMAIL);
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(membre));
        ApproveTransactionRequest req = new ApproveTransactionRequest("Approuvé");

        TransactionApprovalResponse result = approvalService.approveTransaction(approvalId, req);

        assertThat(result).isNotNull();
        assertThat(result.getStatus()).isEqualTo("VALIDATED");
        assertThat(result.getHasAllApprovals()).isTrue();
        verify(approvalRepository).persist(approval);
    }

    @Test
    @DisplayName("approveTransaction_partialApprovals_setsApproved")
    void approveTransaction_partialApprovals_setsApproved() {
        // LEVEL3 → 3 approbations requises, aucune existante → après 1 approbation = APPROVED
        TransactionApproval approval = createMockApproval();
        approval.setRequiredLevel("LEVEL3");
        UUID approvalId = approval.getId();
        when(approvalRepository.findByIdOptional(approvalId)).thenReturn(Optional.of(approval));
        Membre membre = buildMembre(USER_ID, USER_EMAIL);
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(membre));
        ApproveTransactionRequest req = new ApproveTransactionRequest("Premier accord");

        TransactionApprovalResponse result = approvalService.approveTransaction(approvalId, req);

        assertThat(result).isNotNull();
        assertThat(result.getStatus()).isEqualTo("APPROVED");
        verify(approvalRepository).persist(approval);
    }

    // ─── rejectTransaction ───────────────────────────────────────────────────────

    @Test
    @DisplayName("rejectTransaction_notFound_throwsNotFound")
    void rejectTransaction_notFound_throwsNotFound() {
        when(approvalRepository.findByIdOptional(any())).thenReturn(Optional.empty());
        RejectTransactionRequest req = new RejectTransactionRequest("Montant incorrect");

        assertThatThrownBy(() -> approvalService.rejectTransaction(UUID.randomUUID(), req))
                .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("rejectTransaction_memberNotFound_throwsForbidden — couvre lambda$rejectTransaction$6")
    void rejectTransaction_memberNotFound_throwsForbidden() {
        // Approval valide (PENDING)
        TransactionApproval approval = createMockApproval();
        UUID approvalId = approval.getId();
        when(approvalRepository.findByIdOptional(approvalId)).thenReturn(Optional.of(approval));
        // Membre introuvable → déclenche lambda$6 : () -> new ForbiddenException("Utilisateur non trouvé")
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.empty());
        RejectTransactionRequest req = new RejectTransactionRequest("Membre absent");

        assertThatThrownBy(() -> approvalService.rejectTransaction(approvalId, req))
                .isInstanceOf(ForbiddenException.class);
    }

    @Test
    @DisplayName("rejectTransaction_approvedStatus_doesNotThrow — branche L222: status=APPROVED → !PENDING=true, !APPROVED=false → condition false")
    void rejectTransaction_approvedStatus_doesNotThrow() {
        TransactionApproval approval = createMockApproval();
        approval.setStatus("APPROVED"); // L222: !"PENDING"=true, !"APPROVED"=false → condition false → ne lève pas
        UUID approvalId = approval.getId();
        when(approvalRepository.findByIdOptional(approvalId)).thenReturn(Optional.of(approval));
        Membre membre = buildMembre(USER_ID, USER_EMAIL);
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(membre));
        RejectTransactionRequest req = new RejectTransactionRequest("Approuvé mais rejeté quand même");

        TransactionApprovalResponse result = approvalService.rejectTransaction(approvalId, req);
        assertThat(result).isNotNull();
        assertThat(result.getStatus()).isEqualTo("REJECTED");
    }

    @Test
    @DisplayName("rejectTransaction_wrongStatus_throwsForbidden")
    void rejectTransaction_wrongStatus_throwsForbidden() {
        TransactionApproval approval = createMockApproval();
        approval.setStatus("VALIDATED"); // ni PENDING ni APPROVED
        UUID approvalId = approval.getId();
        when(approvalRepository.findByIdOptional(approvalId)).thenReturn(Optional.of(approval));
        RejectTransactionRequest req = new RejectTransactionRequest("Refus tardif");

        assertThatThrownBy(() -> approvalService.rejectTransaction(approvalId, req))
                .isInstanceOf(ForbiddenException.class);
    }

    @Test
    @DisplayName("rejectTransaction_success")
    void rejectTransaction_success() {
        TransactionApproval approval = createMockApproval();
        UUID approvalId = approval.getId();
        when(approvalRepository.findByIdOptional(approvalId)).thenReturn(Optional.of(approval));
        Membre membre = buildMembre(USER_ID, USER_EMAIL);
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(membre));
        RejectTransactionRequest req = new RejectTransactionRequest("Justification insuffisante");

        TransactionApprovalResponse result = approvalService.rejectTransaction(approvalId, req);

        assertThat(result).isNotNull();
        assertThat(result.getStatus()).isEqualTo("REJECTED");
        assertThat(result.getRejectionReason()).isEqualTo("Justification insuffisante");
        verify(approvalRepository).persist(approval);
    }

    // ─── getApprovalsHistory ─────────────────────────────────────────────────────

    @Test
    @DisplayName("getApprovalsHistory_nullOrgId_throwsIllegalArgument")
    void getApprovalsHistory_nullOrgId_throwsIllegalArgument() {
        assertThatThrownBy(() -> approvalService.getApprovalsHistory(null, null, null, null))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    @DisplayName("getApprovalsHistory_withOrgId_returnsList")
    void getApprovalsHistory_withOrgId_returnsList() {
        TransactionApproval approval = createMockApproval();
        approval.setStatus("VALIDATED");
        LocalDateTime start = LocalDateTime.now().minusDays(30);
        LocalDateTime end = LocalDateTime.now();
        when(approvalRepository.findHistory(ORG_ID, start, end, "VALIDATED"))
                .thenReturn(List.of(approval));

        List<TransactionApprovalResponse> result =
                approvalService.getApprovalsHistory(ORG_ID, start, end, "VALIDATED");

        assertThat(result).hasSize(1);
        verify(approvalRepository).findHistory(ORG_ID, start, end, "VALIDATED");
    }

    // ─── toResponse avec organisation non-null (branche organizationId) ─────────

    @Test
    @DisplayName("requestApproval_withOrg_success — couvre toResponse avec organisation != null (ligne 311)")
    void requestApproval_withOrg_success_coversToResponseWithOrganisation() {
        Membre membre = buildMembre(USER_ID, USER_EMAIL);
        when(membreRepository.findByEmail(USER_EMAIL)).thenReturn(Optional.of(membre));
        Organisation org = new Organisation();
        org.setId(ORG_ID);
        when(organisationRepository.findByIdOptional(ORG_ID)).thenReturn(Optional.of(org));

        // requestApproval avec org non null → approval.getOrganisation() != null → organizationId = org.getId()
        TransactionApprovalResponse result = approvalService.requestApproval(
                UUID.randomUUID(), "CONTRIBUTION", 200_000.0, ORG_ID);

        assertThat(result).isNotNull();
        assertThat(result.getOrganizationId()).isEqualTo(ORG_ID);
    }

    // ─── countPendingApprovals ───────────────────────────────────────────────────

    @Test
    @DisplayName("countPendingApprovals_withOrgId_callsRepo")
    void countPendingApprovals_withOrgId_callsRepo() {
        when(approvalRepository.countPendingByOrganisation(ORG_ID)).thenReturn(5L);

        long count = approvalService.countPendingApprovals(ORG_ID);

        assertThat(count).isEqualTo(5L);
        verify(approvalRepository).countPendingByOrganisation(ORG_ID);
    }

    @Test
    @DisplayName("countPendingApprovals_withoutOrgId_callsCount")
    void countPendingApprovals_withoutOrgId_callsCount() {
        when(approvalRepository.count("status", "PENDING")).thenReturn(12L);

        long count = approvalService.countPendingApprovals(null);

        assertThat(count).isEqualTo(12L);
        verify(approvalRepository).count("status", "PENDING");
    }
}
