package cloud.nstart.terravault.ordinis.restapi.idempotency;

import cloud.nstart.terravault.ordinis.domain.idempotency.IdempotencyKey;
import cloud.nstart.terravault.ordinis.domain.idempotency.IdempotencyKeyRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataIntegrityViolationException;

import java.time.OffsetDateTime;
import java.util.Optional;

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

class IdempotencyServiceTest {

  private IdempotencyKeyRepository repo;
  private IdempotencyService service;

  @BeforeEach
  void setUp() {
    repo = mock(IdempotencyKeyRepository.class);
    service = new IdempotencyService(repo, new ObjectMapper());
  }

  // ============= hashRequest =============

  @Test
  void hashIsDeterministicForSameInput() {
    byte[] body = "{\"a\":1}".getBytes();
    assertThat(service.hashRequest(body)).isEqualTo(service.hashRequest(body));
  }

  @Test
  void hashDiffersForDifferentInput() {
    assertThat(service.hashRequest("{\"a\":1}".getBytes()))
        .isNotEqualTo(service.hashRequest("{\"a\":2}".getBytes()));
  }

  @Test
  void hashHandlesNullAsEmpty() {
    assertThat(service.hashRequest(null)).isEqualTo(service.hashRequest(new byte[0]));
  }

  @Test
  void hashIsSha256Hex() {
    String hash = service.hashRequest("hello".getBytes());
    assertThat(hash).hasSize(64).matches("[0-9a-f]{64}");
  }

  // ============= lookup =============

  @Test
  void lookupReturnsEmptyWhenKeyMissing() {
    when(repo.findById("k1")).thenReturn(Optional.empty());
    assertThat(service.lookup("k1", "hash")).isEmpty();
  }

  @Test
  void lookupReturnsEntryWhenHashMatches() {
    var key = new IdempotencyKey("k1", "hash-A", OffsetDateTime.now());
    when(repo.findById("k1")).thenReturn(Optional.of(key));
    assertThat(service.lookup("k1", "hash-A")).contains(key);
  }

  @Test
  void lookupThrows422OnHashMismatch() {
    var key = new IdempotencyKey("k1", "hash-A", OffsetDateTime.now());
    when(repo.findById("k1")).thenReturn(Optional.of(key));
    assertThatThrownBy(() -> service.lookup("k1", "hash-B"))
        .isInstanceOf(IdempotencyConflictException.class);
  }

  // ============= reserve =============

  @Test
  void reserveCallsSaveAndFlush() {
    service.reserve("k1", "hash-A");
    verify(repo, times(1)).saveAndFlush(any(IdempotencyKey.class));
  }

  @Test
  void reserveOnRaceWithSameHashThrowsInProgress() {
    var existing = new IdempotencyKey("k1", "hash-A", OffsetDateTime.now());
    when(repo.saveAndFlush(any(IdempotencyKey.class)))
        .thenThrow(new DataIntegrityViolationException("dup"));
    when(repo.findById("k1")).thenReturn(Optional.of(existing));

    assertThatThrownBy(() -> service.reserve("k1", "hash-A"))
        .isInstanceOf(IdempotencyInProgressException.class);
  }

  @Test
  void reserveOnRaceWithDifferentHashThrowsConflict() {
    var existing = new IdempotencyKey("k1", "hash-other", OffsetDateTime.now());
    when(repo.saveAndFlush(any(IdempotencyKey.class)))
        .thenThrow(new DataIntegrityViolationException("dup"));
    when(repo.findById("k1")).thenReturn(Optional.of(existing));

    assertThatThrownBy(() -> service.reserve("k1", "hash-A"))
        .isInstanceOf(IdempotencyConflictException.class);
  }

  // ============= saveResponse =============

  @Test
  void saveResponseSkipsMissingKey() {
    when(repo.findById("absent")).thenReturn(Optional.empty());
    service.saveResponse("absent", 200, "{}".getBytes());
    verify(repo, times(0)).save(any(IdempotencyKey.class));
  }

  @Test
  void saveResponsePersistsParsedBody() {
    var key = new IdempotencyKey("k1", "hash", OffsetDateTime.now());
    when(repo.findById("k1")).thenReturn(Optional.of(key));

    service.saveResponse("k1", 201, "{\"id\":\"abc\"}".getBytes());

    assertThat(key.getResponseStatus()).isEqualTo(201);
    assertThat(key.getResponseBody()).isNotNull();
    assertThat(key.getResponseBody().get("id").asText()).isEqualTo("abc");
    verify(repo).save(key);
  }

  @Test
  void saveResponseHandlesNonJsonGracefully() {
    var key = new IdempotencyKey("k1", "hash", OffsetDateTime.now());
    when(repo.findById("k1")).thenReturn(Optional.of(key));

    service.saveResponse("k1", 500, "not-json".getBytes());

    assertThat(key.getResponseStatus()).isEqualTo(500);
    assertThat(key.getResponseBody()).isNull();
    verify(repo).save(key);
  }
}
