package dev.lions.unionflow.server.service.devise;

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.lenient;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import dev.lions.unionflow.server.entity.Devise;
import dev.lions.unionflow.server.entity.TauxChange;
import dev.lions.unionflow.server.repository.TauxChangeRepository;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Optional;
import org.assertj.core.data.Offset;
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.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class DeviseConversionServiceTest {

  @Mock TauxChangeRepository repository;

  private DeviseConversionService service;
  private final LocalDate jour = LocalDate.of(2026, 4, 25);

  @BeforeEach
  void setUp() throws Exception {
    service = new DeviseConversionService();
    Field f = DeviseConversionService.class.getDeclaredField("repository");
    f.setAccessible(true);
    f.set(service, repository);
  }

  private TauxChange t(Devise s, Devise c, String taux, LocalDate date) {
    TauxChange tc = new TauxChange();
    tc.setDeviseSource(s);
    tc.setDeviseCible(c);
    tc.setTaux(new BigDecimal(taux));
    tc.setDateValidite(date);
    return tc;
  }

  // ── Cas trivial : devises identiques ─────────────────────────────────────

  @Test
  @DisplayName("convertir XOF→XOF retourne montant inchangé")
  void identite() {
    BigDecimal r = service.convertir(new BigDecimal("1000"), Devise.XOF, Devise.XOF, jour);
    assertThat(r).isEqualByComparingTo("1000");
  }

  // ── Lookup direct ────────────────────────────────────────────────────────

  @Test
  @DisplayName("convertir EUR→XOF utilise le taux exact disponible")
  void direct() {
    when(repository.trouverExact(Devise.EUR, Devise.XOF, jour))
        .thenReturn(Optional.of(t(Devise.EUR, Devise.XOF, "655.957", jour)));

    BigDecimal r = service.convertir(new BigDecimal("100"), Devise.EUR, Devise.XOF, jour);
    assertThat(r).isEqualByComparingTo("65595.7000");
  }

  // ── Inverse ───────────────────────────────────────────────────────────────

  @Test
  @DisplayName("convertir XOF→EUR utilise inverse si direct absent")
  void inverse() {
    when(repository.trouverExact(Devise.XOF, Devise.EUR, jour)).thenReturn(Optional.empty());
    when(repository.trouverExact(Devise.EUR, Devise.XOF, jour))
        .thenReturn(Optional.of(t(Devise.EUR, Devise.XOF, "655.957", jour)));

    BigDecimal r = service.convertir(new BigDecimal("65595.70"), Devise.XOF, Devise.EUR, jour);
    assertThat(r.doubleValue()).isCloseTo(100.0, Offset.offset(0.01));
  }

  // ── Pivot via XOF ─────────────────────────────────────────────────────────

  @Test
  @DisplayName("convertir EUR→USD utilise pivot via XOF")
  void pivot() {
    // Pas de direct EUR→USD
    when(repository.trouverExact(Devise.EUR, Devise.USD, jour)).thenReturn(Optional.empty());
    when(repository.trouverExact(Devise.USD, Devise.EUR, jour)).thenReturn(Optional.empty());
    // Pivot : EUR→XOF puis XOF→USD
    when(repository.trouverExact(Devise.EUR, Devise.XOF, jour))
        .thenReturn(Optional.of(t(Devise.EUR, Devise.XOF, "655.957", jour)));
    when(repository.trouverExact(Devise.XOF, Devise.USD, jour))
        .thenReturn(Optional.of(t(Devise.XOF, Devise.USD, "0.00165289", jour)));
    lenient().when(repository.trouverExact(Devise.XOF, Devise.EUR, jour))
        .thenReturn(Optional.empty());
    lenient().when(repository.trouverExact(Devise.USD, Devise.XOF, jour))
        .thenReturn(Optional.empty());

    BigDecimal r = service.convertir(new BigDecimal("100"), Devise.EUR, Devise.USD, jour);
    // 100 EUR × 655.957 × 0.00165289 ≈ 108.42 USD
    assertThat(r.doubleValue()).isCloseTo(108.42, Offset.offset(0.5));
  }

  // ── Fallback récent ──────────────────────────────────────────────────────

  @Test
  @DisplayName("convertir utilise le taux le plus récent ≤ date si pas de taux exact")
  void fallbackRecent() {
    when(repository.trouverExact(any(), any(), eq(jour))).thenReturn(Optional.empty());
    when(repository.trouverPlusRecent(Devise.EUR, Devise.XOF, jour))
        .thenReturn(Optional.of(t(Devise.EUR, Devise.XOF, "655.957", jour.minusDays(3))));
    lenient().when(repository.trouverPlusRecent(Devise.XOF, Devise.EUR, jour))
        .thenReturn(Optional.empty());

    BigDecimal r = service.convertir(new BigDecimal("10"), Devise.EUR, Devise.XOF, jour);
    assertThat(r.doubleValue()).isCloseTo(6559.57, Offset.offset(0.01));
  }

  // ── Aucun taux ──────────────────────────────────────────────────────────

  @Test
  @DisplayName("convertir sans aucun taux disponible → exception")
  void aucunTaux() {
    when(repository.trouverExact(any(), any(), any())).thenReturn(Optional.empty());
    when(repository.trouverPlusRecent(any(), any(), any())).thenReturn(Optional.empty());

    assertThatThrownBy(() ->
        service.convertir(new BigDecimal("100"), Devise.GHS, Devise.NGN, jour))
        .isInstanceOf(DeviseConversionService.TauxIntrouvableException.class);
  }

  // ── Cache ──────────────────────────────────────────────────────────────

  @Test
  @DisplayName("appels successifs même paire/date utilisent le cache")
  void cache() {
    when(repository.trouverExact(Devise.EUR, Devise.XOF, jour))
        .thenReturn(Optional.of(t(Devise.EUR, Devise.XOF, "655.957", jour)));

    service.convertir(new BigDecimal("1"), Devise.EUR, Devise.XOF, jour);
    service.convertir(new BigDecimal("2"), Devise.EUR, Devise.XOF, jour);
    service.convertir(new BigDecimal("3"), Devise.EUR, Devise.XOF, jour);

    // 1 seul accès repository (cache après le premier appel)
    verify(repository, times(1)).trouverExact(Devise.EUR, Devise.XOF, jour);
  }

  @Test
  @DisplayName("invaliderCache force un rechargement du repository")
  void invaliderCache() {
    when(repository.trouverExact(Devise.EUR, Devise.XOF, jour))
        .thenReturn(Optional.of(t(Devise.EUR, Devise.XOF, "655.957", jour)));

    service.convertir(new BigDecimal("1"), Devise.EUR, Devise.XOF, jour);
    service.invaliderCache();
    service.convertir(new BigDecimal("1"), Devise.EUR, Devise.XOF, jour);

    verify(repository, times(2)).trouverExact(Devise.EUR, Devise.XOF, jour);
  }

  // ── Inputs invalides ──────────────────────────────────────────────────

  @Test
  @DisplayName("montant null → IllegalArgumentException")
  void montantNull() {
    assertThatThrownBy(() -> service.convertir(null, Devise.EUR, Devise.XOF, jour))
        .isInstanceOf(IllegalArgumentException.class);
  }

  @Test
  @DisplayName("devise null → IllegalArgumentException")
  void deviseNull() {
    assertThatThrownBy(() -> service.convertir(BigDecimal.ONE, null, Devise.XOF, jour))
        .isInstanceOf(IllegalArgumentException.class);
  }
}
