package de.szut.casino.dice;

import de.szut.casino.shared.service.BalanceService;
import de.szut.casino.user.UserEntity;
import org.junit.jupiter.api.BeforeEach;
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.math.RoundingMode;
import java.util.Random;

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

@ExtendWith(MockitoExtension.class)
class DiceServiceTest {

    @Mock
    private BalanceService balanceService;

    @Mock
    private Random random;

    @InjectMocks
    private DiceService diceService;

    private UserEntity user;
    private DiceDto diceDto;

    @BeforeEach
    void setUp() {
        user = new UserEntity();
        user.setId(1L);
        user.setBalance(BigDecimal.valueOf(1000));

        diceDto = new DiceDto();
        diceDto.setBetAmount(BigDecimal.valueOf(10));
        diceDto.setTargetValue(BigDecimal.valueOf(50));
        diceDto.setRollOver(true);
    }

    @Test
    void play_rollOver_win() {
        diceDto.setRollOver(true);
        diceDto.setTargetValue(BigDecimal.valueOf(50));
        when(random.nextInt(anyInt())).thenReturn(55);

        DiceResult result = diceService.play(user, diceDto);

        assertTrue(result.isWin());
        assertEquals(BigDecimal.valueOf(56), result.getRolledValue());
        verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
        verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
    }

    @Test
    void play_rollOver_lose() {
        diceDto.setRollOver(true);
        diceDto.setTargetValue(BigDecimal.valueOf(50));
        when(random.nextInt(anyInt())).thenReturn(49);

        DiceResult result = diceService.play(user, diceDto);

        assertFalse(result.isWin());
        assertEquals(BigDecimal.valueOf(50), result.getRolledValue());
        assertEquals(BigDecimal.ZERO, result.getPayout());
        verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
        verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
    }

    @Test
    void play_rollUnder_win() {
        diceDto.setRollOver(false);
        diceDto.setTargetValue(BigDecimal.valueOf(50));
        when(random.nextInt(anyInt())).thenReturn(48);

        DiceResult result = diceService.play(user, diceDto);

        assertTrue(result.isWin());
        assertEquals(BigDecimal.valueOf(49), result.getRolledValue());
        verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
        verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
    }

    @Test
    void play_rollUnder_lose() {
        diceDto.setRollOver(false);
        diceDto.setTargetValue(BigDecimal.valueOf(50));
        when(random.nextInt(anyInt())).thenReturn(50);

        DiceResult result = diceService.play(user, diceDto);

        assertFalse(result.isWin());
        assertEquals(BigDecimal.valueOf(51), result.getRolledValue());
        assertEquals(BigDecimal.ZERO, result.getPayout());
        verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
        verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
    }

    @Test
    void play_rollOver_targetValueOne_rolledOne_lose() {
        diceDto.setRollOver(true);
        diceDto.setTargetValue(BigDecimal.valueOf(1));
        when(random.nextInt(anyInt())).thenReturn(0);

        DiceResult result = diceService.play(user, diceDto);

        assertFalse(result.isWin());
        assertEquals(BigDecimal.valueOf(1), result.getRolledValue());
        assertEquals(BigDecimal.ZERO, result.getPayout());
        verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
        verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
    }

    @Test
    void play_rollOver_targetValueOne_rolledTwo_win() {
        diceDto.setRollOver(true);
        diceDto.setTargetValue(BigDecimal.valueOf(1));
        when(random.nextInt(anyInt())).thenReturn(1);

        DiceResult result = diceService.play(user, diceDto);

        assertTrue(result.isWin());
        assertEquals(BigDecimal.valueOf(2), result.getRolledValue());
        // Win chance for target 1 (roll over) is 99. Multiplier = (100-1)/99 = 1
        assertEquals(diceDto.getBetAmount().stripTrailingZeros(), result.getPayout().stripTrailingZeros());
        verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
        verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
    }

    @Test
    void play_rollUnder_targetValueOne_alwaysLose_winChanceZero() {
        diceDto.setRollOver(false);
        diceDto.setTargetValue(BigDecimal.valueOf(1));
        when(random.nextInt(anyInt())).thenReturn(0);

        DiceResult result = diceService.play(user, diceDto);

        assertFalse(result.isWin());
        assertEquals(BigDecimal.valueOf(1), result.getRolledValue());
        assertEquals(BigDecimal.ZERO, result.getPayout());
        verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
        verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
    }

    @Test
    void play_rollOver_targetValueNinetyNine_rolledHundred_win() {
        diceDto.setRollOver(true);
        diceDto.setTargetValue(BigDecimal.valueOf(99));
        when(random.nextInt(anyInt())).thenReturn(99);

        DiceResult result = diceService.play(user, diceDto);

        assertTrue(result.isWin());
        assertEquals(BigDecimal.valueOf(100), result.getRolledValue());
        // Win chance for target 99 (roll over) is 1. Multiplier = (100-1)/1 = 99
        assertEquals(diceDto.getBetAmount().multiply(BigDecimal.valueOf(99)).stripTrailingZeros(), result.getPayout().stripTrailingZeros());
        verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
        verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
    }

    @Test
    void play_rollUnder_targetValueNinetyNine_rolledNinetyEight_win() {
        diceDto.setRollOver(false);
        diceDto.setTargetValue(BigDecimal.valueOf(99));
        when(random.nextInt(anyInt())).thenReturn(97);

        DiceResult result = diceService.play(user, diceDto);

        assertTrue(result.isWin());
        assertEquals(BigDecimal.valueOf(98), result.getRolledValue());
        // Win chance for target 99 (roll under) is 98. Multiplier = (100-1)/98 = 99/98
        assertEquals(diceDto.getBetAmount().multiply(BigDecimal.valueOf(99).divide(BigDecimal.valueOf(98), 4, RoundingMode.HALF_UP)), result.getPayout());
        verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
        verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
    }

    @Test
    void play_rollOver_targetValueOneHundred_alwaysLose_winChanceZero() {
        diceDto.setRollOver(true);
        diceDto.setTargetValue(BigDecimal.valueOf(100));
        when(random.nextInt(anyInt())).thenReturn(99);

        DiceResult result = diceService.play(user, diceDto);

        assertFalse(result.isWin());
        assertEquals(BigDecimal.valueOf(100), result.getRolledValue());
        assertEquals(BigDecimal.ZERO, result.getPayout());
        verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
        verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
    }

    @Test
    void play_rollUnder_targetValueOneHundred_rolledNinetyNine_win() {
        diceDto.setRollOver(false);
        diceDto.setTargetValue(BigDecimal.valueOf(100));
        when(random.nextInt(anyInt())).thenReturn(98);

        DiceResult result = diceService.play(user, diceDto);

        assertTrue(result.isWin());
        assertEquals(BigDecimal.valueOf(99), result.getRolledValue());
        // Win chance for target 100 (roll under) is 99. Multiplier = (100-1)/99 = 1
        assertEquals(diceDto.getBetAmount().stripTrailingZeros(), result.getPayout().stripTrailingZeros());
        verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
        verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
    }

    @Test
    void play_payoutCalculationCorrect() {
        diceDto.setRollOver(true);
        diceDto.setTargetValue(BigDecimal.valueOf(75));
        when(random.nextInt(anyInt())).thenReturn(75);

        // Multiplier for win chance 25: (100-1)/25 = 99/25 = 3.96
        // Payout: 10 * 3.96 = 39.6

        DiceResult result = diceService.play(user, diceDto);

        assertTrue(result.isWin());
        assertEquals(BigDecimal.valueOf(39.6).stripTrailingZeros(), result.getPayout().stripTrailingZeros());
    }

    @Test
    void play_payoutCalculationCorrect_rollUnder() {
        diceDto.setRollOver(false);
        diceDto.setTargetValue(BigDecimal.valueOf(25));
        when(random.nextInt(anyInt())).thenReturn(0);

        // Multiplier for win chance 24: (100-1)/24 = 99/24 = 4.125
        // Payout: 10 * 4.125 = 41.25

        DiceResult result = diceService.play(user, diceDto);

        assertTrue(result.isWin());
        assertEquals(BigDecimal.valueOf(41.25).stripTrailingZeros(), result.getPayout().stripTrailingZeros());
    }

    @Test
    void play_betAmountSubtracted() {
        when(random.nextInt(anyInt())).thenReturn(50);

        diceService.play(user, diceDto);

        verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
    }
}
