/*
 * StringUtilTests.java
 *
 * Copyright (C) 2022 by Posit Software, PBC
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */
package org.rstudio.core.client;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.junit.client.GWTTestCase;

public class StringUtilTests extends GWTTestCase
{
   @Override
   public String getModuleName()
   {
      return "org.rstudio.studio.RStudioTests";
   }
   
   // -- StringUtil.padRight tests --------------------------------------------

   public void testPadRightNullInput()
   {
      String orig = null;
      int minWidth = 5;
      try
      {
         StringUtil.padRight(orig, minWidth);
         fail("Expected exception to be thrown");
      } 
      catch (NullPointerException ex) { }
      catch (JavaScriptException ex) { }
   }

   public void testPadRightEmptyInput()
   {
      String empty = "";
      String twentyFiveSpaces = "                         ";
      int minWidth = 25;
      String result = StringUtil.padRight(empty, minWidth);
      assertTrue(result.length() == minWidth);
      assertEquals(result, twentyFiveSpaces);
   }

   public void testPadRightSpaces()
   {
      String twoSpaces = "  ";
      String fourSpaces = "    ";
      int minWidth = 4;
      String result = StringUtil.padRight(twoSpaces,  minWidth);
      assertTrue(result.length() == minWidth);
      assertEquals(result, fourSpaces);
   }

   public void testPadRightAlreadyLonger()
   {
      String original = "12345";
      int minWidth = 4;
      assertTrue(original.length() > minWidth);
      String result = StringUtil.padRight(original, minWidth);
      assertTrue(result.length() == original.length());
   }

   // -- StringUtil.parseInt tests --------------------------------------------

   public void testParseIntNullInput()
   {
      String input = null;
      int def = 999;
      int result = StringUtil.parseInt(input, def);
      assertEquals(def, result);
   }

   public void testParseIntNonNumericInput()
   {
      String notAnInt = "hello";
      int def = 125;
      int result = StringUtil.parseInt(notAnInt, def);
      assertEquals(def, result);
   }

   public void testParseIntValidPositive()
   {
      String anInt = "5433234";
      int expected =  5433234;
      int def = 0;
      assertFalse(def == expected);
      int result = StringUtil.parseInt(anInt, def);
      assertEquals(expected, result);
   }

   public void testParseIntOutOfRange()
   {
      String aHugeNumber = "999999999999999999";
      int expected = 256;
      int def = expected;
      int result = StringUtil.parseInt(aHugeNumber, def);
      assertEquals(expected, result);
   }

   public void testParseIntNegative()
   {
      String anInt = "-1";
      int expected =  -1;
      int def = 1;
      assertFalse(def == expected);
      int result = StringUtil.parseInt(anInt, def);
      assertEquals(expected, result);
   }

   // -- StringUtil.formatDate tests  -----------------------------------------

   public void testFormatDateNullInput()
   {
      Date input = null;
      String expected = "";
      String result = StringUtil.formatDate(input);
      assertEquals(expected, result);
   }

   public void testFormatDate()
   {
      // formatDate now delegates to the browser Intl API, whose exact output
      // (length, 12h/24h, separators) varies by locale, timezone, and time of
      // day. Use a fixed date and assert the result is non-empty and contains
      // the year
      Date input = new Date(1592222400000L); // 2020-06-15T12:00:00Z
      String result = StringUtil.formatDate(input);
      assertFalse(StringUtil.isNullOrEmpty(result));
      assertTrue(result.contains("2020"));
   }
   
   public void testNewlineCount()
   {
      String input = "hello\nworld\n";
      assertEquals(2, StringUtil.newlineCount(input));
      
      input = "nothing here";
      assertEquals(0, StringUtil.newlineCount(input));
      
      input = "\n\n\nzoom\n";
      assertEquals(4, StringUtil.newlineCount(input));
      
      input = "";
      assertEquals(0, StringUtil.newlineCount(input));
   }

   public void testGetAuthorityFromUrl()
   {
      String url = "http://posit.co/products";
      assertEquals("posit.co", StringUtil.getAuthorityFromUrl(url));
      
      url = "https://google.com";
      assertEquals("google.com", StringUtil.getAuthorityFromUrl(url));
      
      url = "8.8.8.8:443/notfound.html";
      assertEquals("8.8.8.8:443", StringUtil.getAuthorityFromUrl(url));
   }
   
   public void testGetHostFromUrl()
   {
      String url = "8.8.8.8:443/notfound.html";
      assertEquals("8.8.8.8", StringUtil.getHostFromUrl(url));
      
      url = "https://localhost:443/";
      assertEquals("localhost", StringUtil.getHostFromUrl(url));

      url = "https://bakersfield/";
      assertEquals("bakersfield", StringUtil.getHostFromUrl(url));
   }
   
   public void testStringEquals()
   {
      String one_1 = "one";
      String two_1 = "two";
      String one_2 = "one";
      String two_2 = "two";
      String null_1 = null;
      String null_2 = null;
      
      assertTrue(StringUtil.equals(one_1, one_2));
      assertTrue(StringUtil.equals(one_2, one_1)); 
      assertTrue(StringUtil.equals(two_1, two_2));
      assertTrue(StringUtil.equals(two_2, two_1));
      
      assertTrue(StringUtil.equals(null_1, null_2));
      assertTrue(StringUtil.equals(null_2, null_1));
      
      assertTrue(StringUtil.equals(one_1, one_1));
      assertTrue(StringUtil.equals(one_2, one_2));
      assertTrue(StringUtil.equals(two_1, two_1));
      assertTrue(StringUtil.equals(two_2, two_2));
      
      assertTrue(StringUtil.equals(null_1, null_1));
      assertTrue(StringUtil.equals(null_2, null_2));
      
      assertFalse(StringUtil.equals(one_1, two_1));
      assertFalse(StringUtil.equals(two_1, one_1));
      assertFalse(StringUtil.equals(one_2, two_2));
      assertFalse(StringUtil.equals(two_2, one_2));
      
      assertFalse(StringUtil.equals(one_1, null_1));
      assertFalse(StringUtil.equals(two_1, null_1));
      assertFalse(StringUtil.equals(one_1, null_1));
      assertFalse(StringUtil.equals(two_1, null_1));
      
      assertFalse(StringUtil.equals(one_2, null_1));
      assertFalse(StringUtil.equals(two_2, null_1));
      assertFalse(StringUtil.equals(one_2, null_1));
      assertFalse(StringUtil.equals(two_2, null_1));
      
      assertFalse(StringUtil.equals(null_2, one_1));
      assertFalse(StringUtil.equals(null_2, two_1));
      assertFalse(StringUtil.equals(null_2, one_2));
      assertFalse(StringUtil.equals(null_2, two_2));
   } 
   
   public void testConciseElapsedTime()
   {
      String seconds = StringUtil.conciseElaspedTime(35);
      assertEquals("0:35", seconds);

      String minutes = StringUtil.conciseElaspedTime(66);
      assertEquals("1:06", minutes);

      String hours = StringUtil.conciseElaspedTime(3606);
      assertEquals("1:00:06", hours);
      
      String days = StringUtil.conciseElaspedTime(180061);
      assertEquals("2:02:01:01", days);
   }
  
   public void testGetCssIdentifier()
   {
      List<Pair<String, String>> testList = new ArrayList<>();
      testList.add(new Pair<>("4abc_bad",        "_abc_bad"));
      testList.add(new Pair<>("--verybad",       "_-verybad"));
      testList.add(new Pair<>("-2badagain?",     "_2badagain_"));
      testList.add(new Pair<>("great342_-↲",     "great342_-↲"));
      testList.add(new Pair<>("-_perfectlyfine", "-_perfectlyfine"));

      for (Pair<String, String> td  : testList)
      {
         String result = StringUtil.getCssIdentifier(td.first);
         assertTrue(StringUtil.equals(td.second, result));
      }
   }

   public void testEscapeBashPathNoSpecialChars()
   {
      String input = "NothingSpecialHere.129,._+@%/-";
      String expected = input;
      String result = StringUtil.escapeBashPath(input, true);
      assertTrue(StringUtil.equals(result, expected));
   }
   
   public void testEscapeBashPathNoSpecialCharsNoTilde()
   {
      String input = "NothingSpecialHere.129,._+@%/-";
      String expected = input;
      String result = StringUtil.escapeBashPath(input, true);
      assertTrue(StringUtil.equals(result, expected));
   }
   
   public void testEscapeBashPathSpacesAndEmbeddedTilde()
   {
      String input = " ~/Something Special Here. 129, ._ +@%/- ";
      String expected = "\\ \\~/Something\\ Special\\ Here.\\ 129,\\ ._\\ +@%/-\\ ";
      String result = StringUtil.escapeBashPath(input, true);
      assertTrue(StringUtil.equals(result, expected));
   }
   
   public void testEscapeBashPathSpecialAndUnescapedTilde()
   {
      String input = "~/Something Special Here! 129, ._ +@%/->";
      String expected = "~/Something\\ Special\\ Here\\!\\ 129,\\ ._\\ +@%/-\\>";
      String result = StringUtil.escapeBashPath(input, false);
      assertTrue(StringUtil.equals(result, expected));
   }
   
   public void testEscapeBashPathSpecialAndEscapedTilde()
   {
      String input = "~/Something Special Here! 129, ._ +@%/->";
      String expected = "\\~/Something\\ Special\\ Here\\!\\ 129,\\ ._\\ +@%/-\\>";
      String result = StringUtil.escapeBashPath(input, true);
      assertTrue(StringUtil.equals(result, expected));
   }
   
   public void testIsCharAt()
   {
      assertTrue(StringUtil.isCharAt("abcd", 'a', 0));
      assertTrue(StringUtil.isCharAt("abcd", 'b', 1));
      assertTrue(StringUtil.isCharAt("abcd", 'c', 2));
      assertTrue(StringUtil.isCharAt("abcd", 'd', 3));
   }
   
   public void testIsCharAtOOB()
   {
      assertFalse(StringUtil.isCharAt("abcd", 'a', -1));
      assertFalse(StringUtil.isCharAt(null, 'a', 0));
      assertFalse(StringUtil.isCharAt("012345", '6', 6));
   }
   
   public void testSafeCharAt()
   {
      String str = "";
      
      assertEquals(StringUtil.charAt(str, 0), '\0');
      assertEquals(StringUtil.charAt(str, -1), '\0');
      assertEquals(StringUtil.charAt(str, 100), '\0');
      
      str = "abcd";

      assertEquals(StringUtil.charAt(str, 0), 'a');
      assertEquals(StringUtil.charAt(str, 1), 'b');
      assertEquals(StringUtil.charAt(str, 2), 'c');
      assertEquals(StringUtil.charAt(str, 3), 'd');
   }

   public void testCollapseWithoutPrefix()
   {
        Map<String, String> testData = new HashMap<>();
        testData.put("key1", "value1");
        testData.put("key2", "value2");
        testData.put("key3", "value3");
        testData.put("key4", "value4");
        testData.put("key5", "value5");

        String expected = "key1 :-: value1\nkey2 :-: value2\nkey3 :-: value3\nkey4 :-: value4\nkey5 :-: value5";
        String result = StringUtil.collapse(testData, " :-: ", "\n");
        assertEquals(expected, result);
   }

   public void testCollapseWithPrefix()
   {
        Map<String, String> testData = new HashMap<>();
        testData.put("key1", "value1");
        testData.put("key2", "value2");
        testData.put("key3", "value3");
        testData.put("key4", "value4");
        testData.put("key5", "value5");

        String expected = ">> key1 :-: value1\n>> key2 :-: value2\n>> key3 :-: value3\n>> key4 :-: value4\n>> key5 :-: value5";
        String result = StringUtil.collapse(testData, ">> ", " :-: ", "\n");
        assertEquals(expected, result);
   }

   public void testCommonPrefix()
   {
      String[] lines = new String[] {
         "#' @param parameter",
         "#'",
         "#'   This is a line."
      };

      // With phantom whitespace
      assertEquals(
         StringUtil.getCommonPrefix(lines, true, false),
         "#' "
      );

      // Without phantom whitespace
      assertEquals(
         StringUtil.getCommonPrefix(lines, false, false),
         "#'"
      );
   }

   // https://github.com/rstudio/rstudio/issues/17493
   public void testCommonPrefixWhitespaceOnlyLines()
   {
      // A single whitespace-only line should still report its own indent as the
      // common prefix when skipWhitespaceOnlyLines is enabled; otherwise the
      // comment shortcut loses the indent and inserts "#" at column zero. The
      // allowPhantomWhitespace argument is irrelevant here: the single-line case
      // returns the line verbatim before any prefix comparison happens.
      assertEquals(
         "    ",
         StringUtil.getCommonPrefix(new String[] { "    " }, true, true)
      );

      // Tabs are a realistic indent for the comment shortcut, and must be
      // preserved the same way as spaces.
      assertEquals(
         "\t",
         StringUtil.getCommonPrefix(new String[] { "\t" }, true, true)
      );

      // When every line is whitespace-only, fall back to comparing them rather
      // than collapsing the prefix to the empty string. allowPhantomWhitespace
      // must be false so the shorter line bounds the common prefix at "  ";
      // with phantom whitespace the prefix would extend to the longer indent.
      assertEquals(
         "  ",
         StringUtil.getCommonPrefix(new String[] { "  ", "    " }, false, true)
      );

      // With at least one content line, whitespace-only lines are still skipped.
      assertEquals(
         "  ",
         StringUtil.getCommonPrefix(new String[] { "  x", "", "  y" }, false, true)
      );
   }

   // -- StringUtil.htmlUnescape tests ----------------------------------------

   public void testHtmlUnescapeNullAndEmpty()
   {
      assertNull(StringUtil.htmlUnescape(null));
      assertEquals("", StringUtil.htmlUnescape(""));
   }

   public void testHtmlUnescapeNoEntities()
   {
      assertEquals("hello world", StringUtil.htmlUnescape("hello world"));
      assertEquals("plain text", StringUtil.htmlUnescape("plain text"));
   }

   public void testHtmlUnescapeAllEscapes()
   {
      // These are exactly the escapes produced by the server's
      // core::string_utils::htmlEscape (StringUtils.cpp).
      assertEquals("<", StringUtil.htmlUnescape("&lt;"));
      assertEquals(">", StringUtil.htmlUnescape("&gt;"));
      assertEquals("&", StringUtil.htmlUnescape("&amp;"));
      assertEquals("\"", StringUtil.htmlUnescape("&quot;"));
      assertEquals("'", StringUtil.htmlUnescape("&#x27;"));
      assertEquals("/", StringUtil.htmlUnescape("&#x2F;"));
   }

   public void testHtmlUnescapeXssPayloads()
   {
      // Compiler-controlled text that must round-trip via the server-side
      // htmlEscape and the client-side htmlUnescape we just added.
      assertEquals(
         "<script>alert(1)</script>",
         StringUtil.htmlUnescape("&lt;script&gt;alert(1)&lt;&#x2F;script&gt;"));
      assertEquals(
         "<img src=x onerror=alert(1)>",
         StringUtil.htmlUnescape("&lt;img src=x onerror=alert(1)&gt;"));
   }

   public void testHtmlUnescapeAmpersandLast()
   {
      // The encoder emits &amp; for '&' and &lt; for '<'. So the literal
      // input "&lt;" round-trips through encode -> "&amp;lt;" -> decode.
      // Decoding must NOT collapse "&amp;lt;" all the way to "<": that would
      // mean an escaped ampersand had been over-decoded.
      assertEquals("&lt;", StringUtil.htmlUnescape("&amp;lt;"));
      assertEquals("&amp;", StringUtil.htmlUnescape("&amp;amp;"));
      assertEquals("&", StringUtil.htmlUnescape("&amp;"));
   }

   public void testHtmlUnescapeUnknownEntityPreserved()
   {
      // Entities we don't recognize are passed through untouched.
      assertEquals("&unknown;", StringUtil.htmlUnescape("&unknown;"));
      assertEquals("&#999;", StringUtil.htmlUnescape("&#999;"));
      assertEquals("a & b", StringUtil.htmlUnescape("a & b"));
   }

   public void testHtmlUnescapeCaseSensitive()
   {
      // The server only emits lowercase entity names, so the decoder is
      // case-sensitive. Uppercase variants are passed through untouched.
      assertEquals("&LT;", StringUtil.htmlUnescape("&LT;"));
      assertEquals("&AMP;", StringUtil.htmlUnescape("&AMP;"));
   }

   public void testHtmlUnescapeAdjacentEntities()
   {
      // Adjacent entities must each be decoded independently; no run-on
      // matching across entity boundaries.
      assertEquals("&&", StringUtil.htmlUnescape("&amp;&amp;"));
      assertEquals("<>", StringUtil.htmlUnescape("&lt;&gt;"));
      assertEquals("<<", StringUtil.htmlUnescape("&lt;&lt;"));
   }
}
