package br.gov.sigefp.budget.service;

import br.gov.sigefp.budget.api.dto.BudgetExecutionDTO;
import br.gov.sigefp.budget.domain.BudgetExecution;
import br.gov.sigefp.budget.domain.BudgetLine;
import br.gov.sigefp.budget.domain.FiscalYear;
import br.gov.sigefp.budget.repository.BudgetAllocationRepository;
import br.gov.sigefp.budget.repository.BudgetExecutionRepository;
import br.gov.sigefp.budget.repository.BudgetLineRepository;
import br.gov.sigefp.common.exception.BusinessException;
import br.gov.sigefp.common.exception.InsufficientBudgetException;
import br.gov.sigefp.common.exception.ResourceNotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;
import java.util.Optional;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class BudgetExecutionServiceTest {

    @Mock
    private BudgetExecutionRepository budgetExecutionRepository;

    @Mock
    private BudgetLineRepository budgetLineRepository;

    @Mock
    private BudgetAllocationRepository budgetAllocationRepository;

    @InjectMocks
    private BudgetExecutionService budgetExecutionService;

    private UUID budgetLineId;
    private BudgetLine budgetLine;
    private FiscalYear fiscalYear;

    @BeforeEach
    void setUp() {
        budgetLineId = UUID.randomUUID();
        fiscalYear = FiscalYear.builder()
                .year(2024)
                .status("OPEN")
                .build();

        budgetLine = BudgetLine.builder()
                .id(budgetLineId)
                .code("01.01.01")
                .fiscalYear(fiscalYear)
                .build();
    }

    @Test
    @DisplayName("Deve registrar execução com sucesso quando há saldo")
    void registerExecution_Success() {
        BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
                .budgetLineId(budgetLineId)
                .movementType("COMMITMENT")
                .amount(new BigDecimal("1000"))
                .periodId(202401L)
                .build();

        when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
        when(budgetAllocationRepository.calculateTotalAllocatedByBudgetLineId(budgetLineId))
                .thenReturn(new BigDecimal("5000"));
        when(budgetExecutionRepository.calculateTotalCommittedByBudgetLineId(budgetLineId))
                .thenReturn(new BigDecimal("2000"));

        when(budgetExecutionRepository.save(any(BudgetExecution.class))).thenAnswer(i -> {
            BudgetExecution e = i.getArgument(0);
            e.setId(UUID.randomUUID());
            return e;
        });

        BudgetExecutionDTO result = budgetExecutionService.registerExecution(dto);

        assertNotNull(result);
        assertEquals(new BigDecimal("1000"), result.getAmount());
        verify(budgetExecutionRepository).save(any(BudgetExecution.class));
    }

    @Test
    @DisplayName("Deve falhar ao registrar COMMITMENT se não houver saldo disponível")
    void registerExecution_InsufficientFunds() {
        BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
                .budgetLineId(budgetLineId)
                .movementType("COMMITMENT")
                .amount(new BigDecimal("4000"))
                .build();

        when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
        when(budgetAllocationRepository.calculateTotalAllocatedByBudgetLineId(budgetLineId))
                .thenReturn(new BigDecimal("5000"));
        when(budgetExecutionRepository.calculateTotalCommittedByBudgetLineId(budgetLineId))
                .thenReturn(new BigDecimal("2000")); // Disponível = 3000

        InsufficientBudgetException exception = assertThrows(InsufficientBudgetException.class,
                () -> budgetExecutionService.registerExecution(dto));

        assertTrue(exception.getMessage().contains("Saldo insuficiente"));
        verify(budgetExecutionRepository, never()).save(any());
    }

    @Test
    @DisplayName("Deve falhar ao registrar execução em exercício fiscal fechado")
    void registerExecution_ClosedYear() {
        fiscalYear.setStatus("CLOSED");
        BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
                .budgetLineId(budgetLineId)
                .build();

        when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));

        BusinessException exception = assertThrows(BusinessException.class,
                () -> budgetExecutionService.registerExecution(dto));

        assertEquals("Não é possível registrar movimentos em um ano fiscal fechado", exception.getMessage());
    }

    @Test
    @DisplayName("Deve falhar ao criar LIQUIDATION sem COMMITMENT correspondente")
    void registerExecution_LiquidationWithoutCommitment() {
        UUID referenceId = UUID.randomUUID();
        BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
                .budgetLineId(budgetLineId)
                .movementType("LIQUIDATION")
                .amount(new BigDecimal("1000"))
                .referenceId(referenceId)
                .periodId(202401L)
                .sourceModule("rh")
                .build();

        when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
        when(budgetExecutionRepository.calculateTotalCommittedByReferenceId(referenceId))
                .thenReturn(BigDecimal.ZERO); // Sem COMMITMENT

        BusinessException exception = assertThrows(BusinessException.class,
                () -> budgetExecutionService.registerExecution(dto));

        assertTrue(exception.getMessage().contains("Não existe COMMITMENT correspondente"));
        verify(budgetExecutionRepository, never()).save(any());
    }

    @Test
    @DisplayName("Deve falhar ao criar LIQUIDATION sem referenceId")
    void registerExecution_LiquidationWithoutReferenceId() {
        BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
                .budgetLineId(budgetLineId)
                .movementType("LIQUIDATION")
                .amount(new BigDecimal("1000"))
                .referenceId(null) // Sem referenceId
                .periodId(202401L)
                .sourceModule("rh")
                .build();

        when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));

        BusinessException exception = assertThrows(BusinessException.class,
                () -> budgetExecutionService.registerExecution(dto));

        assertTrue(exception.getMessage().contains("ReferenceId é obrigatório"));
        verify(budgetExecutionRepository, never()).save(any());
    }

    @Test
    @DisplayName("Deve falhar ao criar LIQUIDATION excedendo COMMITMENT")
    void registerExecution_LiquidationExceedingCommitment() {
        UUID referenceId = UUID.randomUUID();
        BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
                .budgetLineId(budgetLineId)
                .movementType("LIQUIDATION")
                .amount(new BigDecimal("2000"))
                .referenceId(referenceId)
                .periodId(202401L)
                .sourceModule("rh")
                .build();

        when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
        when(budgetExecutionRepository.calculateTotalCommittedByReferenceId(referenceId))
                .thenReturn(new BigDecimal("1000")); // COMMITMENT de 1000
        when(budgetExecutionRepository.calculateTotalLiquidatedByReferenceId(referenceId))
                .thenReturn(BigDecimal.ZERO); // Nenhuma liquidação ainda

        BusinessException exception = assertThrows(BusinessException.class,
                () -> budgetExecutionService.registerExecution(dto));

        assertTrue(exception.getMessage().contains("Liquidação não pode exceder empenho"));
        verify(budgetExecutionRepository, never()).save(any());
    }

    @Test
    @DisplayName("Deve falhar ao criar PAYMENT sem LIQUIDATION correspondente")
    void registerExecution_PaymentWithoutLiquidation() {
        UUID referenceId = UUID.randomUUID();
        BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
                .budgetLineId(budgetLineId)
                .movementType("PAYMENT")
                .amount(new BigDecimal("1000"))
                .referenceId(referenceId)
                .periodId(202401L)
                .sourceModule("treasury")
                .build();

        when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
        when(budgetExecutionRepository.calculateTotalLiquidatedByReferenceId(referenceId))
                .thenReturn(BigDecimal.ZERO); // Sem LIQUIDATION

        BusinessException exception = assertThrows(BusinessException.class,
                () -> budgetExecutionService.registerExecution(dto));

        assertTrue(exception.getMessage().contains("Não existe LIQUIDATION correspondente"));
        verify(budgetExecutionRepository, never()).save(any());
    }

    @Test
    @DisplayName("Deve falhar ao criar PAYMENT sem referenceId")
    void registerExecution_PaymentWithoutReferenceId() {
        BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
                .budgetLineId(budgetLineId)
                .movementType("PAYMENT")
                .amount(new BigDecimal("1000"))
                .referenceId(null) // Sem referenceId
                .periodId(202401L)
                .sourceModule("treasury")
                .build();

        when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));

        BusinessException exception = assertThrows(BusinessException.class,
                () -> budgetExecutionService.registerExecution(dto));

        assertTrue(exception.getMessage().contains("ReferenceId é obrigatório"));
        verify(budgetExecutionRepository, never()).save(any());
    }

    @Test
    @DisplayName("Deve falhar ao criar PAYMENT excedendo LIQUIDATION")
    void registerExecution_PaymentExceedingLiquidation() {
        UUID referenceId = UUID.randomUUID();
        BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
                .budgetLineId(budgetLineId)
                .movementType("PAYMENT")
                .amount(new BigDecimal("2000"))
                .referenceId(referenceId)
                .periodId(202401L)
                .sourceModule("treasury")
                .build();

        when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
        when(budgetExecutionRepository.calculateTotalLiquidatedByReferenceId(referenceId))
                .thenReturn(new BigDecimal("1000")); // LIQUIDATION de 1000
        when(budgetExecutionRepository.calculateTotalPaidByReferenceId(referenceId))
                .thenReturn(BigDecimal.ZERO); // Nenhum pagamento ainda

        BusinessException exception = assertThrows(BusinessException.class,
                () -> budgetExecutionService.registerExecution(dto));

        assertTrue(exception.getMessage().contains("Pagamento não pode exceder liquidação"));
        verify(budgetExecutionRepository, never()).save(any());
    }

    @Test
    @DisplayName("Deve registrar LIQUIDATION com sucesso quando há COMMITMENT correspondente")
    void registerExecution_LiquidationSuccess() {
        UUID referenceId = UUID.randomUUID();
        BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
                .budgetLineId(budgetLineId)
                .movementType("LIQUIDATION")
                .amount(new BigDecimal("1000"))
                .referenceId(referenceId)
                .periodId(202401L)
                .sourceModule("rh")
                .build();

        when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
        when(budgetExecutionRepository.calculateTotalCommittedByReferenceId(referenceId))
                .thenReturn(new BigDecimal("2000")); // COMMITMENT de 2000
        when(budgetExecutionRepository.calculateTotalLiquidatedByReferenceId(referenceId))
                .thenReturn(BigDecimal.ZERO); // Nenhuma liquidação ainda

        when(budgetExecutionRepository.save(any(BudgetExecution.class))).thenAnswer(i -> {
            BudgetExecution e = i.getArgument(0);
            e.setId(UUID.randomUUID());
            return e;
        });

        BudgetExecutionDTO result = budgetExecutionService.registerExecution(dto);

        assertNotNull(result);
        assertEquals(new BigDecimal("1000"), result.getAmount());
        assertEquals("LIQUIDATION", result.getMovementType());
        verify(budgetExecutionRepository).save(any(BudgetExecution.class));
    }

    @Test
    @DisplayName("Deve registrar PAYMENT com sucesso quando há LIQUIDATION correspondente")
    void registerExecution_PaymentSuccess() {
        UUID referenceId = UUID.randomUUID();
        BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
                .budgetLineId(budgetLineId)
                .movementType("PAYMENT")
                .amount(new BigDecimal("1000"))
                .referenceId(referenceId)
                .periodId(202401L)
                .sourceModule("treasury")
                .build();

        when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
        when(budgetExecutionRepository.calculateTotalLiquidatedByReferenceId(referenceId))
                .thenReturn(new BigDecimal("2000")); // LIQUIDATION de 2000
        when(budgetExecutionRepository.calculateTotalPaidByReferenceId(referenceId))
                .thenReturn(BigDecimal.ZERO); // Nenhum pagamento ainda

        when(budgetExecutionRepository.save(any(BudgetExecution.class))).thenAnswer(i -> {
            BudgetExecution e = i.getArgument(0);
            e.setId(UUID.randomUUID());
            return e;
        });

        BudgetExecutionDTO result = budgetExecutionService.registerExecution(dto);

        assertNotNull(result);
        assertEquals(new BigDecimal("1000"), result.getAmount());
        assertEquals("PAYMENT", result.getMovementType());
        verify(budgetExecutionRepository).save(any(BudgetExecution.class));
    }
}
