JsonBuilderFactoryTest.java

package redis.clients.jedis.json;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Arrays;
import java.util.List;

import org.junit.jupiter.api.Test;
import redis.clients.jedis.exceptions.JedisException;
import redis.clients.jedis.util.SafeEncoder;

/**
 * Unit tests for JsonBuilderFactory, specifically for the JSON_NUMBER_LIST builder that preserves
 * numeric types (Long for integers, Double for decimals).
 */
public class JsonBuilderFactoryTest {

  @Test
  public void jsonNumberListPreservesLongTypes() {
    // Simulates RESP3 response where Redis returns Long for integers
    List<Object> input = Arrays.asList(1L, 2L, 3L, 100L);
    List<Number> result = JsonBuilderFactory.JSON_NUMBER_LIST.build(input);

    assertEquals(4, result.size());
    for (Number n : result) {
      assertTrue(n instanceof Long, "Expected Long but got " + n.getClass().getName());
    }
    assertEquals(1L, result.get(0));
    assertEquals(2L, result.get(1));
    assertEquals(3L, result.get(2));
    assertEquals(100L, result.get(3));
  }

  @Test
  public void jsonNumberListPreservesDoubleTypes() {
    // Simulates RESP3 response where Redis returns Double for decimals
    List<Object> input = Arrays.asList(1.5, 2.5, 3.14159, 100.0);
    List<Number> result = JsonBuilderFactory.JSON_NUMBER_LIST.build(input);

    assertEquals(4, result.size());
    for (Number n : result) {
      assertTrue(n instanceof Double, "Expected Double but got " + n.getClass().getName());
    }
    assertEquals(1.5, result.get(0));
    assertEquals(2.5, result.get(1));
    assertEquals(3.14159, result.get(2));
    assertEquals(100.0, result.get(3));
  }

  @Test
  public void jsonNumberListPreservesMixedTypes() {
    // Simulates RESP3 response with mixed Long and Double values
    List<Object> input = Arrays.asList(1L, 2.5, 3L, 4.0);
    List<Number> result = JsonBuilderFactory.JSON_NUMBER_LIST.build(input);

    assertEquals(4, result.size());
    assertTrue(result.get(0) instanceof Long, "First element should be Long");
    assertTrue(result.get(1) instanceof Double, "Second element should be Double");
    assertTrue(result.get(2) instanceof Long, "Third element should be Long");
    assertTrue(result.get(3) instanceof Double, "Fourth element should be Double");

    assertEquals(1L, result.get(0));
    assertEquals(2.5, result.get(1));
    assertEquals(3L, result.get(2));
    assertEquals(4.0, result.get(3));
  }

  @Test
  public void jsonNumberListHandlesNullElements() {
    // Simulates response with null values (for non-numeric JSON paths)
    List<Object> input = Arrays.asList(null, 1L, null, 2.5);
    List<Number> result = JsonBuilderFactory.JSON_NUMBER_LIST.build(input);

    assertEquals(4, result.size());
    assertNull(result.get(0));
    assertEquals(1L, result.get(1));
    assertNull(result.get(2));
    assertEquals(2.5, result.get(3));
  }

  @Test
  public void jsonNumberListHandlesNullInput() {
    List<Number> result = JsonBuilderFactory.JSON_NUMBER_LIST.build(null);
    assertNull(result);
  }

  @Test
  public void jsonNumberListParsesStringIntegers() {
    // Simulates RESP2 response where numbers come as byte arrays/strings
    List<Object> input = Arrays.asList(SafeEncoder.encode("1"), SafeEncoder.encode("2"),
      SafeEncoder.encode("100"));
    List<Number> result = JsonBuilderFactory.JSON_NUMBER_LIST.build(input);

    assertEquals(3, result.size());
    for (Number n : result) {
      assertTrue(n instanceof Long, "Parsed integer string should be Long");
    }
    assertEquals(1L, result.get(0));
    assertEquals(2L, result.get(1));
    assertEquals(100L, result.get(2));
  }

  @Test
  public void jsonNumberListParsesStringDecimals() {
    // Simulates RESP2 response where decimal numbers come as byte arrays/strings
    List<Object> input = Arrays.asList(SafeEncoder.encode("1.5"), SafeEncoder.encode("2.5"),
      SafeEncoder.encode("3.14159"));
    List<Number> result = JsonBuilderFactory.JSON_NUMBER_LIST.build(input);

    assertEquals(3, result.size());
    for (Number n : result) {
      assertTrue(n instanceof Double, "Parsed decimal string should be Double");
    }
    assertEquals(1.5, result.get(0));
    assertEquals(2.5, result.get(1));
    assertEquals(3.14159, result.get(2));
  }

  @Test
  public void jsonNumberListParsesScientificNotation() {
    // Test parsing of scientific notation numbers
    List<Object> input = Arrays.asList(SafeEncoder.encode("1e10"), SafeEncoder.encode("2.5E-3"));
    List<Number> result = JsonBuilderFactory.JSON_NUMBER_LIST.build(input);

    assertEquals(2, result.size());
    assertTrue(result.get(0) instanceof Double, "Scientific notation should be Double");
    assertTrue(result.get(1) instanceof Double, "Scientific notation should be Double");
    assertEquals(1e10, result.get(0));
    assertEquals(2.5e-3, result.get(1));
  }

  @Test
  public void jsonArrayOrDoubleListReturnsNumberListForListInput() {
    // Test that JSON_ARRAY_OR_DOUBLE_LIST uses JSON_NUMBER_LIST for List input
    List<Object> input = Arrays.asList(1L, 2.5, 3L);
    Object result = JsonBuilderFactory.JSON_ARRAY_OR_NUMBER_LIST.build(input);

    assertTrue(result instanceof List, "Result should be a List");
    @SuppressWarnings("unchecked")
    List<Number> numberList = (List<Number>) result;

    assertEquals(3, numberList.size());
    assertTrue(numberList.get(0) instanceof Long, "First element should be Long");
    assertTrue(numberList.get(1) instanceof Double, "Second element should be Double");
    assertTrue(numberList.get(2) instanceof Long, "Third element should be Long");
  }

  // ===== Tests for NUMBER_LIST builder (RESP2 JSON array parsing) =====

  @Test
  public void numberListParsesJsonArrayOfIntegers() {
    byte[] data = SafeEncoder.encode("[1,2,3]");
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(data);

    assertEquals(3, result.size());
    assertEquals(1L, result.get(0));
    assertEquals(2L, result.get(1));
    assertEquals(3L, result.get(2));
  }

  @Test
  public void numberListParsesJsonArrayOfDecimals() {
    byte[] data = SafeEncoder.encode("[1.5,2.5,3.14]");
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(data);

    assertEquals(3, result.size());
    assertEquals(1.5, result.get(0));
    assertEquals(2.5, result.get(1));
    assertEquals(3.14, result.get(2));
  }

  @Test
  public void numberListParsesJsonArrayWithNullValues() {
    byte[] data = SafeEncoder.encode("[1,null,3.5]");
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(data);

    assertEquals(3, result.size());
    assertEquals(1L, result.get(0));
    assertNull(result.get(1));
    assertEquals(3.5, result.get(2));
  }

  @Test
  public void numberListParsesJsonArrayWithScientificNotation() {
    byte[] data = SafeEncoder.encode("[1e10,2.5E-3,3.0e2]");
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(data);

    assertEquals(3, result.size());
    assertEquals(1e10, result.get(0));
    assertEquals(2.5e-3, result.get(1));
    assertEquals(3.0e2, result.get(2));
  }

  @Test
  public void numberListParsesEmptyJsonArray() {
    byte[] data = SafeEncoder.encode("[]");
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(data);

    assertEquals(0, result.size());
  }

  @Test
  public void numberListHandlesWhitespace() {
    byte[] data = SafeEncoder.encode("[ 1 , null , 3.5 ]");
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(data);

    assertEquals(3, result.size());
    assertEquals(1L, result.get(0));
    assertNull(result.get(1));
    assertEquals(3.5, result.get(2));
  }

  @Test
  public void numberListHandlesLeadingAndTrailingWhitespace() {
    byte[] data = SafeEncoder.encode("  [ 1 , 2 ]  ");
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(data);

    assertEquals(2, result.size());
    assertEquals(1L, result.get(0));
    assertEquals(2L, result.get(1));
  }

  @Test
  public void numberListParsesNegativeNumbers() {
    byte[] data = SafeEncoder.encode("[-1,-2.5,-3e2]");
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(data);

    assertEquals(3, result.size());
    assertEquals(-1L, result.get(0));
    assertEquals(-2.5, result.get(1));
    assertEquals(-3e2, result.get(2));
  }

  @Test
  public void numberListParsesLargeNumbers() {
    byte[] data = SafeEncoder.encode("[9223372036854775807,1.7976931348623157e308]");
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(data);

    assertEquals(2, result.size());
    assertEquals(Long.MAX_VALUE, result.get(0));
    assertEquals(Double.MAX_VALUE, result.get(1));
  }

  @Test
  public void numberListRejectsEmptyElements() {
    byte[] data = SafeEncoder.encode("[1,,3]");
    assertThrows(JedisException.class, () -> JsonBuilderFactory.NUMBER_LIST.build(data),
      "Should reject empty elements");
  }

  @Test
  public void numberListRejectsTrailingComma() {
    byte[] data = SafeEncoder.encode("[1,2,]");
    assertThrows(JedisException.class, () -> JsonBuilderFactory.NUMBER_LIST.build(data),
      "Should reject trailing comma");
  }

  @Test
  public void numberListRejectsLeadingComma() {
    byte[] data = SafeEncoder.encode("[,1,2]");
    assertThrows(JedisException.class, () -> JsonBuilderFactory.NUMBER_LIST.build(data),
      "Should reject leading comma");
  }

  @Test
  public void numberListRejectsInvalidJson() {
    byte[] data = SafeEncoder.encode("not an array");
    assertThrows(JedisException.class, () -> JsonBuilderFactory.NUMBER_LIST.build(data),
      "Should reject invalid JSON");
  }

  @Test
  public void numberListRejectsInvalidNumbers() {
    byte[] data = SafeEncoder.encode("[1,abc,3]");
    assertThrows(JedisException.class, () -> JsonBuilderFactory.NUMBER_LIST.build(data),
      "Should reject non-numeric values");
  }

  @Test
  public void numberListHandlesNullInput() {
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(null);
    assertNull(result);
  }

  @Test
  public void numberListHandlesAllNullArray() {
    byte[] data = SafeEncoder.encode("[null,null,null]");
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(data);

    assertEquals(3, result.size());
    assertNull(result.get(0));
    assertNull(result.get(1));
    assertNull(result.get(2));
  }

  @Test
  public void numberListHandlesMixedWhitespaceVariations() {
    byte[] data = SafeEncoder.encode("[\n  1  ,\t  null\t,  3.5  \n]");
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(data);

    assertEquals(3, result.size());
    assertEquals(1L, result.get(0));
    assertNull(result.get(1));
    assertEquals(3.5, result.get(2));
  }

  @Test
  public void numberListPreservesNumericTypes() {
    byte[] data = SafeEncoder.encode("[42,3.14,null,-100,1e5]");
    List<Number> result = JsonBuilderFactory.NUMBER_LIST.build(data);

    assertEquals(5, result.size());
    assertTrue(result.get(0) instanceof Long, "Integer should be Long");
    assertTrue(result.get(1) instanceof Double, "Decimal should be Double");
    assertNull(result.get(2));
    assertTrue(result.get(3) instanceof Long, "Negative integer should be Long");
    assertTrue(result.get(4) instanceof Double, "Scientific notation should be Double");
  }
}