CollectReducerTest.java

package redis.clients.jedis.search.aggr;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;

import redis.clients.jedis.Protocol;
import redis.clients.jedis.search.SearchProtocol.SearchKeyword;

/**
 * Unit tests for {@link CollectReducer} ��� verify that the builder produces the exact token layout
 * that the Redis Search COLLECT grammar expects.
 */
public class CollectReducerTest {

  // -- defaults / metadata -------------------------------------------------

  @Test
  public void newReducerHasCollectNameAndNoField() {
    CollectReducer collect = Reducers.collect();
    assertEquals("COLLECT", collect.getName());
    assertNull(collect.getField(), "COLLECT does not use the parent's single-field slot");
    assertNull(collect.getAlias());
  }

  @Test
  public void aliasIsRecordedOnTheReducer() {
    CollectReducer collect = Reducers.collect().fields("@fruit");
    Reducer same = collect.as("items");
    assertSame(collect, same, "as() returns the same instance for chaining");
    assertEquals("items", collect.getAlias());
  }

  // -- FIELDS form ---------------------------------------------------------

  @Test
  public void fieldsExplicitProducesNumFieldsAndNames() {
    // REDUCE COLLECT 4 FIELDS 2 @fruit @sweetness AS items
    CollectReducer collect = Reducers.collect().fields("@fruit", "@sweetness");
    collect.as("items");

    List<Object> args = serialize(collect);
    assertThat(args, contains(SearchKeyword.REDUCE, "COLLECT", 4, SearchKeyword.FIELDS, 2, "@fruit",
      "@sweetness", SearchKeyword.AS, "items"));
  }

  @Test
  public void fieldsAllProducesAsteriskWithoutCount() {
    // REDUCE COLLECT 2 FIELDS * AS top
    CollectReducer collect = Reducers.collect().fieldsAll();
    collect.as("top");

    List<Object> args = serialize(collect);
    assertThat(args, contains(SearchKeyword.REDUCE, "COLLECT", 2, SearchKeyword.FIELDS,
      Protocol.BYTES_ASTERISK, SearchKeyword.AS, "top"));
  }

  @Test
  public void fieldsCanBeAppendedAcrossCalls() {
    CollectReducer collect = Reducers.collect().fields("@a", "@b").fields("@c");
    collect.as("items");

    List<Object> args = serialize(collect);
    // narg = FIELDS 3 @a @b @c = 5 tokens
    assertEquals(5, args.get(2));
    assertThat(args, contains(SearchKeyword.REDUCE, "COLLECT", 5, SearchKeyword.FIELDS, 3, "@a",
      "@b", "@c", SearchKeyword.AS, "items"));
  }

  @Test
  public void fieldsAllAfterExplicitFieldsThrows() {
    assertThrows(IllegalStateException.class,
      () -> Reducers.collect().fields("@fruit").fieldsAll());
  }

  @Test
  public void explicitFieldsAfterFieldsAllThrows() {
    assertThrows(IllegalStateException.class,
      () -> Reducers.collect().fieldsAll().fields("@fruit"));
  }

  @Test
  public void missingFieldsConfigurationThrowsAtSerialization() {
    CollectReducer collect = Reducers.collect();
    collect.as("items");
    assertThrows(IllegalStateException.class, () -> collect.addArgs(new ArrayList<>()));
  }

  // -- SORTBY --------------------------------------------------------------

  @Test
  public void sortByDescOnly() {
    // narg = FIELDS 1 @fruit SORTBY 2 @sweetness DESC = 7
    CollectReducer collect = Reducers.collect().fields("@fruit").sortByDesc("@sweetness");
    collect.as("top");

    List<Object> args = serialize(collect);
    assertThat(args, contains(SearchKeyword.REDUCE, "COLLECT", 7, SearchKeyword.FIELDS, 1, "@fruit",
      SearchKeyword.SORTBY, 2, "@sweetness", "DESC", SearchKeyword.AS, "top"));
  }

  @Test
  public void sortByAscShortcut() {
    CollectReducer collect = Reducers.collect().fields("@fruit").sortByAsc("@sweetness");
    collect.as("top");

    List<Object> args = serialize(collect);
    int sortByIdx = args.indexOf(SearchKeyword.SORTBY);
    assertTrue(sortByIdx > 0, "SORTBY token must appear in the serialized args");
    assertEquals(2, args.get(sortByIdx + 1));
    assertEquals("@sweetness", args.get(sortByIdx + 2));
    assertEquals("ASC", args.get(sortByIdx + 3));
  }

  @Test
  public void sortByMultipleFieldsAccumulates() {
    // narg = FIELDS 1 @x SORTBY 4 @a DESC @b ASC = 9
    CollectReducer collect = Reducers.collect().fields("@x").sortBy(SortedField.desc("@a"),
      SortedField.asc("@b"));
    collect.as("top");

    List<Object> args = serialize(collect);
    assertThat(args, contains(SearchKeyword.REDUCE, "COLLECT", 9, SearchKeyword.FIELDS, 1, "@x",
      SearchKeyword.SORTBY, 4, "@a", "DESC", "@b", "ASC", SearchKeyword.AS, "top"));
  }

  @Test
  public void successiveSortByCallsAppend() {
    CollectReducer collect = Reducers.collect().fields("@x").sortBy(SortedField.desc("@a"))
        .sortBy(SortedField.asc("@b"));
    collect.as("top");

    List<Object> args = serialize(collect);
    int sortByIdx = args.indexOf(SearchKeyword.SORTBY);
    // 2 sort fields �� 2 tokens each = 4
    assertEquals(4, args.get(sortByIdx + 1));
  }

  // -- LIMIT ---------------------------------------------------------------

  @Test
  public void limitCountOnlyDefaultsOffsetZero() {
    // narg = FIELDS 1 @x LIMIT 0 5 = 6
    CollectReducer collect = Reducers.collect().fields("@x").limit(5);
    collect.as("top");

    List<Object> args = serialize(collect);
    assertThat(args, contains(SearchKeyword.REDUCE, "COLLECT", 6, SearchKeyword.FIELDS, 1, "@x",
      SearchKeyword.LIMIT, 0, 5, SearchKeyword.AS, "top"));
  }

  @Test
  public void limitOffsetCount() {
    CollectReducer collect = Reducers.collect().fields("@x").limit(3, 7);
    collect.as("top");

    List<Object> args = serialize(collect);
    int limitIdx = args.indexOf(SearchKeyword.LIMIT);
    assertEquals(3, args.get(limitIdx + 1));
    assertEquals(7, args.get(limitIdx + 2));
  }

  @Test
  public void limitRejectsNegativeOffset() {
    assertThrows(IllegalArgumentException.class, () -> Reducers.collect().limit(-1, 5));
  }

  @Test
  public void limitRejectsNegativeCount() {
    assertThrows(IllegalArgumentException.class, () -> Reducers.collect().limit(0, -1));
  }

  // -- Full clause ---------------------------------------------------------

  @Test
  public void fullClauseMatchesGrammarOrdering() {
    // FIELDS comes first, then SORTBY, then LIMIT, then AS ��� even if methods were called
    // in a different order on the builder.
    CollectReducer collect = Reducers.collect().limit(0, 2).sortByDesc("@sweetness")
        .fields("@__key", "@fruit", "@sweetness").sortBy(SortedField.asc("@__key"));
    collect.as("top");

    List<Object> args = serialize(collect);
    // narg = 14: FIELDS 3 @__key @fruit @sweetness SORTBY 4 @sweetness DESC @__key ASC LIMIT 0 2
    assertThat(args,
      contains(SearchKeyword.REDUCE, "COLLECT", 14, SearchKeyword.FIELDS, 3, "@__key", "@fruit",
        "@sweetness", SearchKeyword.SORTBY, 4, "@sweetness", "DESC", "@__key", "ASC",
        SearchKeyword.LIMIT, 0, 2, SearchKeyword.AS, "top"));
  }

  @Test
  public void nargMatchesNumberOfTokensBetweenCollectAndAs() {
    CollectReducer collect = Reducers.collect().fieldsAll().sortBy(SortedField.desc("@s")).limit(0,
      2);
    collect.as("top");

    List<Object> args = serialize(collect);
    int collectIdx = args.indexOf("COLLECT");
    int asIdx = args.indexOf(SearchKeyword.AS);
    int declaredNarg = (Integer) args.get(collectIdx + 1);

    assertEquals(asIdx - collectIdx - 2, declaredNarg,
      "narg must equal the number of tokens between COLLECT <narg> and AS");
  }

  @Test
  public void aliasIsOptional() {
    CollectReducer collect = Reducers.collect().fields("@fruit");
    List<Object> args = serialize(collect);
    // No AS / alias appended when not set.
    assertThat(args.contains(SearchKeyword.AS), is(false));
  }

  // -- helpers -------------------------------------------------------------

  private static List<Object> serialize(CollectReducer collect) {
    List<Object> args = new ArrayList<>();
    collect.addArgs(args);
    return args;
  }
}