/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.hudi.metadata;

import org.apache.hudi.common.util.collection.Pair;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.stream.Stream;

import static org.apache.hudi.metadata.SecondaryIndexKeyUtils.getUnescapedSecondaryKeyPrefixFromSecondaryIndexKey;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class TestSecondaryIndexKeyUtils {

  private static String generateString(String str, int count) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < count; i++) {
      sb.append(str);
    }
    return sb.toString();
  }

  // Combined round-trip test for escape/unescape
  @ParameterizedTest(name = "Escape/unescape round-trip: input='{0}', expectedEscaped='{1}'")
  @MethodSource("escapeUnescapeRoundTripTestCases")
  public void testEscapeUnescapeRoundTrip(String input, String expectedEscaped) {
    String escaped = SecondaryIndexKeyUtils.escapeSpecialChars(input);
    assertEquals(expectedEscaped, escaped);
    String unescaped = SecondaryIndexKeyUtils.unescapeSpecialChars(escaped);
    if (input == null) {
      assertNull(unescaped);
    } else {
      assertEquals(input, unescaped);
    }
  }

  @SuppressWarnings("checkstyle:IllegalTokenText")
  private static Stream<Arguments> escapeUnescapeRoundTripTestCases() {
    return Stream.of(
        // Null case
        Arguments.of(null, "\0"),
        
        // Empty string
        Arguments.of("", ""),
        
        // Normal strings
        Arguments.of("normal", "normal"),
        Arguments.of("with spaces", "with spaces"),
        Arguments.of("with_underscore", "with_underscore"),
        
        // Special characters that need escaping
        Arguments.of("$", "\\$"),
        Arguments.of("\\", "\\\\"),
        Arguments.of("\0", "\\\0"),
        
        // Multiple special characters
        Arguments.of("$$$", "\\$\\$\\$"),
        Arguments.of("\\\\", "\\\\\\\\"),
        Arguments.of("\0\0\0", "\\\0\\\0\\\0"),
        
        // Mixed special characters
        Arguments.of("$\\", "\\$\\\\"),
        Arguments.of("\\$", "\\\\\\$"),
        Arguments.of("$\0", "\\$\\\0"),
        Arguments.of("\\$\0", "\\\\\\$\\\0"),
        
        // Special characters with normal text
        Arguments.of("user$id", "user\\$id"),
        Arguments.of("path\\to\\file", "path\\\\to\\\\file"),
        Arguments.of("null\0char", "null\\\0char"),
        
        // Complex combinations
        Arguments.of("$start", "\\$start"),
        Arguments.of("$start", "\\$start"),
        Arguments.of("end$", "end\\$"),
        Arguments.of("mid$dle", "mid\\$dle"),
        Arguments.of("\\$mixed$\\", "\\\\\\$mixed\\$\\\\"),
        Arguments.of("all$special\\chars\0here", "all\\$special\\\\chars\\\0here"),
        
        // Unicode and emoji
        Arguments.of("用户$ID", "用户\\$ID"),
        Arguments.of("记录\\123", "记录\\\\123"),
        Arguments.of("😀$emoji\\test\0", "😀\\$emoji\\\\test\\\0"),
        
        // Edge cases
        Arguments.of("end_with_backslash\\", "end_with_backslash\\\\"),
        Arguments.of("\\start_with_backslash", "\\\\start_with_backslash"),
        Arguments.of("$$$$\\\\\\\\\\\\\\\\", "\\$\\$\\$\\$\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"),
        Arguments.of("\0\0$$$\\\\\\", "\\\0\\\0\\$\\$\\$\\\\\\\\\\\\")
    );
  }

  // Test unescapeSpecialChars with strings that can never be generated by escapeSpecialChars
  @Test
  public void testUnescapeSpecialScenarios() {
    // These should not throw exceptions but handle gracefully
    
    // Lone escape at the end is consumed
    assertEquals("test", SecondaryIndexKeyUtils.unescapeSpecialChars("test\\"));
    
    // Multiple escapes
    assertEquals("\\", SecondaryIndexKeyUtils.unescapeSpecialChars("\\\\\\"));
    
    // Escaping non-special characters (escape is consumed, next char is kept)
    assertEquals("a", SecondaryIndexKeyUtils.unescapeSpecialChars("\\a"));
    assertEquals("test", SecondaryIndexKeyUtils.unescapeSpecialChars("\\test"));
    
    // Complex invalid escape sequences
    assertEquals("ab", SecondaryIndexKeyUtils.unescapeSpecialChars("\\a\\b"));
  }

  // Test getUnescapedSecondaryKeyFromSecondaryIndexKey exhaustively
  @ParameterizedTest(name = "Get unescaped secondary key: input='{0}', expected='{1}'")
  @MethodSource("getUnescapedSecondaryKeyTestCases")
  public void testGetUnescapedSecondaryKeyFromSecondaryIndexKey(String encodedKey, String expectedSecondaryKey) {
    String actualSecondaryKey = SecondaryIndexKeyUtils.getUnescapedSecondaryKeyFromSecondaryIndexKey(encodedKey);
    assertEquals(expectedSecondaryKey, actualSecondaryKey);
  }

  private static Stream<Arguments> getUnescapedSecondaryKeyTestCases() {
    return Stream.of(
        // Simple cases
        Arguments.of("key$value", "key"),
        Arguments.of("$value", ""),
        Arguments.of("key$", "key"),
        Arguments.of("$", ""),
        
        // Escaped delimiter in secondary key
        Arguments.of("key\\$part$value", "key\\$part"),
        Arguments.of("\\$start$value", "\\$start"),
        Arguments.of("end\\$$value", "end\\$"),
        Arguments.of("\\$$value", "\\$"),
        
        // Multiple escaped characters
        Arguments.of("\\\\$value", "\\\\"),
        Arguments.of("\\$\\$$value", "\\$\\$"),
        Arguments.of("key\\\\\\$part$value", "key\\\\\\$part"),
        
        // Complex escaped sequences
        Arguments.of("user\\$id\\\\test$record123", "user\\$id\\\\test"),
        Arguments.of("\\\0\\$\\\\$value", "\\\0\\$\\\\"),
        Arguments.of("escaped\\$dollar\\\\backslash$record", "escaped\\$dollar\\\\backslash"),
        
        // Unicode
        Arguments.of("用户\\$ID$记录123", "用户\\$ID"),
        Arguments.of("😀\\$emoji$test", "😀\\$emoji"),
        
        // Edge cases with multiple potential delimiters
        Arguments.of("a$b$c", "a"),  // First unescaped $ is the delimiter
        Arguments.of("a\\$b$c$d", "a\\$b"),  // First unescaped $ after escaped one
        Arguments.of("\\$\\$\\$$real", "\\$\\$\\$"),  // All $ before real delimiter are escaped
        
        // Very long keys
        Arguments.of(generateString("a", 1000) + "$" + generateString("b", 1000), generateString("a", 1000)),
        Arguments.of(generateString("\\$", 100) + "$value", generateString("\\$", 100))
    );
  }

  // Smoke tests for getSecondaryKeyFromSecondaryIndexKey
  @Test
  public void testGetSecondaryKeyFromSecondaryIndexKeySmokeTests() {    
    // Simple case
    String key1 = "user\\$id$record123";
    assertEquals("user$id", SecondaryIndexKeyUtils.getSecondaryKeyFromSecondaryIndexKey(key1));
    
    // Null representation
    String key2 = "\0$record123";
    assertNull(SecondaryIndexKeyUtils.getSecondaryKeyFromSecondaryIndexKey(key2));
    
    // Complex escaped sequence
    String key3 = "\\\\\\$test\\\0$value";
    assertEquals("\\$test\0", SecondaryIndexKeyUtils.getSecondaryKeyFromSecondaryIndexKey(key3));
    
    // Empty secondary key
    String key4 = "$record";
    assertEquals("", SecondaryIndexKeyUtils.getSecondaryKeyFromSecondaryIndexKey(key4));
  }

  // Test full key construction and extraction round-trip
  @ParameterizedTest(name = "Key construction round-trip: secondaryKey='{0}', recordKey='{1}'")
  @MethodSource("keyConstructionRoundTripTestCases")
  public void testKeyConstructionRoundTrip(String secondaryKey, String recordKey) {
    // Construct the key used by the writer path
    String constructedKey = SecondaryIndexKeyUtils.constructSecondaryIndexKey(secondaryKey, recordKey);
    // The key used by the reader path and the key used by the writer path have the following invariant.
    assertEquals(new SecondaryIndexPrefixRawKey(secondaryKey).encode(), getUnescapedSecondaryKeyPrefixFromSecondaryIndexKey(constructedKey));
    
    // Extract both parts
    String extractedSecondary = SecondaryIndexKeyUtils.getSecondaryKeyFromSecondaryIndexKey(constructedKey);
    String extractedRecord = SecondaryIndexKeyUtils.getRecordKeyFromSecondaryIndexKey(constructedKey);
    
    // Verify round-trip
    if (secondaryKey == null) {
      assertNull(extractedSecondary);
    } else {
      assertEquals(secondaryKey, extractedSecondary);
    }
    
    if (recordKey == null) {
      assertNull(extractedRecord);
    } else {
      assertEquals(recordKey, extractedRecord);
    }
    
    // Also test getSecondaryKeyRecordKeyPair and getRecordKeySecondaryKeyPair
    Pair<String, String> secondaryRecordPair = SecondaryIndexKeyUtils.getSecondaryKeyRecordKeyPair(constructedKey);
    Pair<String, String> recordSecondaryPair = SecondaryIndexKeyUtils.getRecordKeySecondaryKeyPair(constructedKey);
    
    if (secondaryKey == null) {
      assertNull(secondaryRecordPair.getLeft());
      assertNull(recordSecondaryPair.getRight());
    } else {
      assertEquals(secondaryKey, secondaryRecordPair.getLeft());
      assertEquals(secondaryKey, recordSecondaryPair.getRight());
    }
    
    if (recordKey == null) {
      assertNull(secondaryRecordPair.getRight());
      assertNull(recordSecondaryPair.getLeft());
    } else {
      assertEquals(recordKey, secondaryRecordPair.getRight());
      assertEquals(recordKey, recordSecondaryPair.getLeft());
    }
  }

  private static Stream<Arguments> keyConstructionRoundTripTestCases() {
    return Stream.of(
        // Normal cases
        Arguments.of("user_id", "record_123"),
        Arguments.of("", ""),
        Arguments.of("simple", "simple"),
        // Null cases
        Arguments.of(null, "record123"),
        Arguments.of("secondary123", null),
        Arguments.of(null, null),
        // Empty string cases
        Arguments.of("", "record_123"),
        Arguments.of("user_id", ""),
        // Special characters
        Arguments.of("user$id", "record$123"),
        Arguments.of("path\\to\\file", "another\\path"),
        Arguments.of("null\0char", "another\0null"),
        Arguments.of("$", "$"),
        Arguments.of("\\", "\\"),
        Arguments.of("\0", "\0"),
        // Complex combinations
        Arguments.of("user\\\\id", "record\\\\123"),
        Arguments.of("user\\$id\\0", "record\\$123\\0"),
        Arguments.of("\\$\\0\\", "\\$\\0\\"),
        Arguments.of("mixed$\\special\0", "chars$\\here\0"),
        Arguments.of("\\$\\0", "\\$\\0"),
        // Unicode
        Arguments.of("用户ID", "记录123"),
        Arguments.of("😀$emoji", "test\\value"),
        // Edge cases
        Arguments.of("user\\$id", "rec\\$123"),
        Arguments.of("\\\\\\$\0", "\\\\\\$\0")
    );
  }

  // Test error cases
  @ParameterizedTest(name = "Invalid key format: '{0}'")
  @ValueSource(strings = {
      "",           // Empty string
      "no_delimiter", // No delimiter
      "\0", // No delimiter
      "\\\0", // No delimiter
      "\\\\", // No delimiter
      "\\", // No delimiter
      "key\\$key",  // Escaped delimiter
      "key\\\\\\$key" // Multiple escaped characters
  })
  public void testInvalidKeyFormats(String invalidKey) {
    assertThrows(IllegalStateException.class, () -> {
      SecondaryIndexKeyUtils.getSecondaryKeyFromSecondaryIndexKey(invalidKey);
    });

    assertThrows(IllegalStateException.class, () -> {
      SecondaryIndexKeyUtils.getRecordKeyFromSecondaryIndexKey(invalidKey);
    });
  }

  // Test boundary conditions
  @Test
  public void testBoundaryConditions() {
    // Test with very long strings
    StringBuilder longSecondaryKeyBuilder = new StringBuilder();
    StringBuilder longRecordKeyBuilder = new StringBuilder();
    for (int i = 0; i < 1000; i++) {
      longSecondaryKeyBuilder.append('a');
      longRecordKeyBuilder.append('b');
    }
    String longSecondaryKey = longSecondaryKeyBuilder.toString();
    String longRecordKey = longRecordKeyBuilder.toString();

    String constructedKey = SecondaryIndexKeyUtils.constructSecondaryIndexKey(longSecondaryKey, longRecordKey);
    assertEquals(longSecondaryKey + "$" + longRecordKey, constructedKey);

    String extractedSecondaryKey = SecondaryIndexKeyUtils.getSecondaryKeyFromSecondaryIndexKey(constructedKey);
    assertEquals(longSecondaryKey, extractedSecondaryKey);

    String extractedRecordKey = SecondaryIndexKeyUtils.getRecordKeyFromSecondaryIndexKey(constructedKey);
    assertEquals(longRecordKey, extractedRecordKey);

    // Test with strings containing only special characters
    String onlySpecialChars = "\\$\\0";
    constructedKey = SecondaryIndexKeyUtils.constructSecondaryIndexKey(onlySpecialChars, onlySpecialChars);
    assertEquals("\\\\\\$\\\\0$\\\\\\$\\\\0", constructedKey);

    extractedSecondaryKey = SecondaryIndexKeyUtils.getSecondaryKeyFromSecondaryIndexKey(constructedKey);
    assertEquals(onlySpecialChars, extractedSecondaryKey);

    extractedRecordKey = SecondaryIndexKeyUtils.getRecordKeyFromSecondaryIndexKey(constructedKey);
    assertEquals(onlySpecialChars, extractedRecordKey);
  }
}
