package dev.lions.unionflow.server.payment.pispi.readiness;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.lenient;
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.payment.pispi.PispiAuth;
import dev.lions.unionflow.server.payment.pispi.PispiSignatureVerifier;
import dev.lions.unionflow.server.payment.pispi.readiness.PispiReadinessService.CheckStatus;
import dev.lions.unionflow.server.payment.pispi.readiness.PispiReadinessService.ReadinessStatus;
import dev.lions.unionflow.server.payment.pispi.readiness.PispiReadinessService.Severity;
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.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 PispiReadinessServiceTest {

  @Mock TauxChangeRepository tauxChangeRepository;

  private PispiReadinessService service;
  private PispiAuth auth;
  private PispiSignatureVerifier verifier;

  @BeforeEach
  void setUp() throws Exception {
    auth = new PispiAuth();
    setField(auth, "clientId", "");
    setField(auth, "clientSecret", "");
    setField(auth, "apiKey", "");
    setField(auth, "keystorePathOpt", Optional.<String>empty());
    setField(auth, "keystorePasswordOpt", Optional.<String>empty());
    setField(auth, "truststorePathOpt", Optional.<String>empty());
    setField(auth, "truststorePasswordOpt", Optional.<String>empty());

    verifier = new PispiSignatureVerifier();
    setField(verifier, "webhookSecret", "");

    service = new PispiReadinessService();
    setField(service, "pispiAuth", auth);
    setField(service, "signatureVerifier", verifier);
    setField(service, "tauxChangeRepository", tauxChangeRepository);
    setField(service, "baseUrl", "https://sandbox.pispi.bceao.int/business-api/v1");
  }

  private void setField(Object target, String fieldName, Object value) throws Exception {
    Field f = findField(target.getClass(), fieldName);
    f.setAccessible(true);
    f.set(target, value);
  }

  private Field findField(Class<?> cls, String name) throws NoSuchFieldException {
    Class<?> c = cls;
    while (c != null) {
      try {
        return c.getDeclaredField(name);
      } catch (NoSuchFieldException e) {
        c = c.getSuperclass();
      }
    }
    throw new NoSuchFieldException(name);
  }

  private TauxChange tauxRecent() {
    TauxChange t = new TauxChange();
    t.setDeviseSource(Devise.EUR);
    t.setDeviseCible(Devise.XOF);
    t.setTaux(new BigDecimal("655.957"));
    t.setDateValidite(LocalDate.now());
    return t;
  }

  // ── Tout vide ────────────────────────────────────────────────────────────

  @Test
  @DisplayName("Tout manquant → BLOCKED + checks failed")
  void toutManquant_blocked() {
    lenient().when(tauxChangeRepository.trouverPlusRecent(Devise.EUR, Devise.XOF, LocalDate.now()))
        .thenReturn(Optional.empty());

    var report = service.verifierReadiness();

    assertThat(report.globalStatus()).isEqualTo(ReadinessStatus.BLOCKED);
    assertThat(report.checks()).hasSize(8);
    assertThat(report.blockingIssues()).hasSizeGreaterThanOrEqualTo(4);
  }

  // ── Tout configuré ──────────────────────────────────────────────────────

  @Test
  @DisplayName("Tout configuré + keystore valide + taux récent → READY")
  void toutOk_ready(@org.junit.jupiter.api.io.TempDir java.nio.file.Path tmp) throws Exception {
    java.nio.file.Path keystore = tmp.resolve("client.p12");
    java.nio.file.Files.writeString(keystore, "fake-keystore");
    java.nio.file.Path truststore = tmp.resolve("trust.p12");
    java.nio.file.Files.writeString(truststore, "fake-trust");

    setField(auth, "clientId", "client-uuid");
    setField(auth, "clientSecret", "secret");
    setField(auth, "apiKey", "api-key-xyz");
    setField(auth, "keystorePathOpt", Optional.of(keystore.toString()));
    setField(auth, "keystorePasswordOpt", Optional.of("kspw"));
    setField(auth, "truststorePathOpt", Optional.of(truststore.toString()));
    setField(auth, "truststorePasswordOpt", Optional.of("tspw"));
    setField(verifier, "webhookSecret", "webhook-hmac-secret");

    when(tauxChangeRepository.trouverPlusRecent(Devise.EUR, Devise.XOF, LocalDate.now()))
        .thenReturn(Optional.of(tauxRecent()));

    var report = service.verifierReadiness();

    assertThat(report.globalStatus()).isEqualTo(ReadinessStatus.READY);
    assertThat(report.blockingIssues()).isEmpty();
    assertThat(report.warnings()).isEmpty();
    assertThat(report.checks()).allMatch(c -> c.status() == CheckStatus.PASS);
  }

  // ── DEGRADED ─────────────────────────────────────────────────────────────

  @Test
  @DisplayName("Tout BLOCKING ok mais taux EUR/XOF absent → DEGRADED")
  void blockingOkWarningKO_degraded(@org.junit.jupiter.api.io.TempDir java.nio.file.Path tmp)
      throws Exception {
    java.nio.file.Path keystore = tmp.resolve("client.p12");
    java.nio.file.Files.writeString(keystore, "fake");
    java.nio.file.Path truststore = tmp.resolve("trust.p12");
    java.nio.file.Files.writeString(truststore, "fake");

    setField(auth, "clientId", "c");
    setField(auth, "clientSecret", "s");
    setField(auth, "apiKey", "k");
    setField(auth, "keystorePathOpt", Optional.of(keystore.toString()));
    setField(auth, "keystorePasswordOpt", Optional.of("p"));
    setField(auth, "truststorePathOpt", Optional.of(truststore.toString()));
    setField(auth, "truststorePasswordOpt", Optional.of("p"));
    setField(verifier, "webhookSecret", "secret");

    when(tauxChangeRepository.trouverPlusRecent(Devise.EUR, Devise.XOF, LocalDate.now()))
        .thenReturn(Optional.empty()); // ← warning

    var report = service.verifierReadiness();

    assertThat(report.globalStatus()).isEqualTo(ReadinessStatus.DEGRADED);
    assertThat(report.blockingIssues()).isEmpty();
    assertThat(report.warnings()).anyMatch(w -> w.contains("TAUX_EUR_XOF"));
  }

  // ── Checks individuels ───────────────────────────────────────────────────

  @Test
  @DisplayName("verifierOAuth2 — fail si client_id vide")
  void oauth2_failClientIdVide() throws Exception {
    setField(auth, "clientId", "");
    setField(auth, "clientSecret", "secret");
    var r = service.verifierOAuth2();
    assertThat(r.status()).isEqualTo(CheckStatus.FAIL);
    assertThat(r.severity()).isEqualTo(Severity.BLOCKING);
  }

  @Test
  @DisplayName("verifierOAuth2 — pass si client_id et client_secret")
  void oauth2_passComplet() throws Exception {
    setField(auth, "clientId", "abc");
    setField(auth, "clientSecret", "def");
    var r = service.verifierOAuth2();
    assertThat(r.status()).isEqualTo(CheckStatus.PASS);
  }

  @Test
  @DisplayName("verifierApiKey — fail si vide, pass sinon")
  void apiKey() throws Exception {
    setField(auth, "apiKey", "");
    assertThat(service.verifierApiKey().status()).isEqualTo(CheckStatus.FAIL);
    setField(auth, "apiKey", "key");
    assertThat(service.verifierApiKey().status()).isEqualTo(CheckStatus.PASS);
  }

  @Test
  @DisplayName("verifierMtlsKeystore — fail si chemin absent")
  void keystore_failPathAbsent() throws Exception {
    setField(auth, "keystorePathOpt", Optional.<String>empty());
    setField(auth, "keystorePasswordOpt", Optional.of("p"));
    assertThat(service.verifierMtlsKeystore().status()).isEqualTo(CheckStatus.FAIL);
  }

  @Test
  @DisplayName("verifierMtlsKeystore — fail si fichier inexistant")
  void keystore_failFichierInexistant() throws Exception {
    setField(auth, "keystorePathOpt", Optional.of("/nonexistent/keystore.p12"));
    setField(auth, "keystorePasswordOpt", Optional.of("p"));
    var r = service.verifierMtlsKeystore();
    assertThat(r.status()).isEqualTo(CheckStatus.FAIL);
    assertThat(r.message()).contains("introuvable");
  }

  @Test
  @DisplayName("verifierMtlsKeystore — pass si path + password + fichier existe")
  void keystore_passComplet(@org.junit.jupiter.api.io.TempDir java.nio.file.Path tmp)
      throws Exception {
    java.nio.file.Path k = tmp.resolve("k.p12");
    java.nio.file.Files.writeString(k, "f");
    setField(auth, "keystorePathOpt", Optional.of(k.toString()));
    setField(auth, "keystorePasswordOpt", Optional.of("pw"));
    assertThat(service.verifierMtlsKeystore().status()).isEqualTo(CheckStatus.PASS);
  }

  @Test
  @DisplayName("verifierTruststore — warning si non configuré")
  void truststore_warningSiAbsent() throws Exception {
    setField(auth, "truststorePathOpt", Optional.<String>empty());
    var r = service.verifierTruststore();
    assertThat(r.status()).isEqualTo(CheckStatus.FAIL);
    assertThat(r.severity()).isEqualTo(Severity.WARNING);
  }

  @Test
  @DisplayName("verifierWebhookSecret — fail si vide")
  void webhook_failVide() throws Exception {
    setField(verifier, "webhookSecret", "");
    assertThat(service.verifierWebhookSecret().status()).isEqualTo(CheckStatus.FAIL);
  }

  @Test
  @DisplayName("verifierBaseUrl — distingue sandbox / production")
  void baseUrl_environnement() throws Exception {
    setField(service, "baseUrl", "https://sandbox.pispi.bceao.int/business-api/v1");
    var rSandbox = service.verifierBaseUrl();
    assertThat(rSandbox.status()).isEqualTo(CheckStatus.PASS);
    assertThat(rSandbox.message()).contains("SANDBOX");

    setField(service, "baseUrl", "https://api.pispi.bceao.int/business-api/v1");
    var rProd = service.verifierBaseUrl();
    assertThat(rProd.message()).contains("PRODUCTION");
  }

  @Test
  @DisplayName("verifierTauxEurXof — fail si taux > 7 jours")
  void tauxEurXof_failObsolete() {
    TauxChange vieux = new TauxChange();
    vieux.setDateValidite(LocalDate.now().minusDays(15));
    vieux.setTaux(new BigDecimal("655.957"));
    when(tauxChangeRepository.trouverPlusRecent(Devise.EUR, Devise.XOF, LocalDate.now()))
        .thenReturn(Optional.of(vieux));

    var r = service.verifierTauxEurXof();
    assertThat(r.status()).isEqualTo(CheckStatus.FAIL);
    assertThat(r.message()).contains("obsolète");
  }

  @Test
  @DisplayName("verifierTauxEurXof — pass si taux récent")
  void tauxEurXof_passRecent() {
    when(tauxChangeRepository.trouverPlusRecent(Devise.EUR, Devise.XOF, LocalDate.now()))
        .thenReturn(Optional.of(tauxRecent()));
    assertThat(service.verifierTauxEurXof().status()).isEqualTo(CheckStatus.PASS);
  }

  @Test
  @DisplayName("verifierProviderConfigured — fail si PispiAuth.isConfigured = false")
  void providerConfigured_fail() throws Exception {
    setField(auth, "clientId", ""); // not configured
    assertThat(service.verifierProviderConfigured().status()).isEqualTo(CheckStatus.FAIL);
  }
}
