/*
 * 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.geode.internal.i18n;

import static org.junit.Assert.*;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;

import org.junit.After;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.i18n.StringId;
import org.apache.geode.test.junit.categories.IntegrationTest;

/**
 * This class tests all basic i18n functionality.
 */
@Category(IntegrationTest.class)
public class BasicI18nJUnitTest {

  private static final Locale DEFAULT_LOCALE = Locale.getDefault();
  // static final Class DEFAULT_RESOURCEBUNDLE = StringIdResourceBundle_ja.class;
  // static final Class JAPAN_RESOURCEBUNDLE = StringIdResourceBundle_ja.class;

  private static final StringId messageId =
      (StringId) LocalizedStrings.TESTING_THIS_IS_A_TEST_MESSAGE;
  private static final String englishMessage = "This is a test message.";
  private static final String japaneseMessage = "msgID " + messageId.id + ": " + "これはテストメッセージである。";

  private static final Integer messageArg = new Integer(1);
  private static final StringId messageIdWithArg =
      (StringId) LocalizedStrings.TESTING_THIS_MESSAGE_HAS_0_MEMBERS;
  private static final String englishMessageWithArg = "Please ignore: This message has 1 members.";
  private static final String japaneseMessageWithArg =
      "msgID " + messageIdWithArg.id + ": Please ignore: このメッセージに 1 メンバーがある。";
  private static final String englishMessageWithArgMissing =
      "Please ignore: This message has {0} members.";
  private static final String japaneseMessageWithArgMissing =
      "msgID " + messageIdWithArg.id + ": Please ignore: このメッセージに {0} メンバーがある。";

  @After
  public void tearDown() {
    // reset to the original
    StringId.setLocale(DEFAULT_LOCALE);
  }

  @Test
  public void testSetLocale() {
    // Verify we are starting in a known state
    assertTrue(DEFAULT_LOCALE.equals(getCurrentLocale()));
    StringId.setLocale(Locale.FRANCE);
    assertTrue(Locale.FRANCE.equals(getCurrentLocale()));

    StringId.setLocale(Locale.FRANCE);
    assertTrue(Locale.FRANCE.equals(getCurrentLocale()));
    StringId s = LocalizedStrings.UNSUPPORTED_AT_THIS_TIME;
    assertTrue(s.toString().equals(s.toLocalizedString()));

    StringId.setLocale(Locale.JAPAN);
    assertTrue(Locale.JAPAN.equals(getCurrentLocale()));
    if (getActiveResourceBundle().usingRawMode()) {
      assertTrue(s.toString().equals(s.toLocalizedString()));
    } else {
      assertFalse(s.toString().equals(s.toLocalizedString()));
    }
  }

  /**
   * Tests {@link StringId#toLocalizedString()} and {@link StringId#toString()} Currently three
   * languages are tested: English, French, and Japanese. French is tested under the assumption that
   * it will never be implemented and thus tests an unimplemented language. Each Locale is tested
   * for these things: 1) toString() no arguments 2) toLocalizedString() no arguments 3)
   * toString(new Integer(1)) one argument 4) toLocalizedString(new Integer(1)) one argument 5)
   * toString() wrong number or arguments, too many 6) toLocalizedString() wrong number or
   * arguments, too many 7) toString() wrong number or arguments, too few 8) toLocalizedString()
   * wrong number or arguments, too few
   */
  @Test
  public void testToLocalizedString() {
    Object[] missingArgs = new Object[] {};
    Object[] extraArgs = new Object[] {messageArg, "should not print"};

    assertEquals(messageId.toString(), englishMessage);
    assertEquals(messageId.toLocalizedString(), englishMessage);
    assertEquals(messageIdWithArg.toString(messageArg), englishMessageWithArg);
    assertEquals(messageIdWithArg.toLocalizedString(messageArg), englishMessageWithArg);
    assertEquals(messageIdWithArg.toString(extraArgs), englishMessageWithArg);
    assertEquals(messageIdWithArg.toLocalizedString(extraArgs), englishMessageWithArg);
    assertEquals(messageIdWithArg.toString(missingArgs), englishMessageWithArgMissing);
    assertEquals(messageIdWithArg.toLocalizedString(missingArgs), englishMessageWithArgMissing);

    /**
     * This is assuming that French isn't implemented and will still get the English resource
     * bundle. The second assert will fail if this isn't the case.
     **/
    StringId.setLocale(Locale.FRANCE);
    assertTrue(Locale.FRANCE.equals(getCurrentLocale()));
    assertEquals(messageId.toString(), englishMessage);
    assertEquals(messageId.toLocalizedString(), englishMessage);
    // Now with a message expecting one argument
    assertEquals(messageIdWithArg.toString(messageArg), englishMessageWithArg);
    assertEquals(messageIdWithArg.toLocalizedString(messageArg), englishMessageWithArg);
    assertEquals(messageIdWithArg.toString(extraArgs), englishMessageWithArg);
    assertEquals(messageIdWithArg.toLocalizedString(extraArgs), englishMessageWithArg);
    assertEquals(messageIdWithArg.toString(missingArgs), englishMessageWithArgMissing);
    assertEquals(messageIdWithArg.toLocalizedString(missingArgs), englishMessageWithArgMissing);

    /**
     * We no longer bundle the JAPAN localized strings with the product so the following now expects
     * english msgs.
     */
    StringId.setLocale(Locale.JAPAN);
    assertTrue(Locale.JAPAN.equals(getCurrentLocale()));
    assertEquals(messageId.toString(), englishMessage);
    if (getActiveResourceBundle().usingRawMode()) {
      assertEquals(messageId.toLocalizedString(), englishMessage);
    } else {
      assertEquals(messageId.toLocalizedString(), japaneseMessage);
    }
    // Now with a message expecting one argument
    assertEquals(messageIdWithArg.toString(messageArg), englishMessageWithArg);
    if (getActiveResourceBundle().usingRawMode()) {
      assertEquals(messageIdWithArg.toLocalizedString(messageArg), englishMessageWithArg);
    } else {
      assertEquals(messageIdWithArg.toLocalizedString(messageArg), japaneseMessageWithArg);
    }
    assertEquals(messageIdWithArg.toString(extraArgs), englishMessageWithArg);
    if (getActiveResourceBundle().usingRawMode()) {
      assertEquals(messageIdWithArg.toLocalizedString(extraArgs), englishMessageWithArg);
    } else {
      assertEquals(messageIdWithArg.toLocalizedString(extraArgs), japaneseMessageWithArg);
    }
    assertEquals(messageIdWithArg.toString(missingArgs), englishMessageWithArgMissing);
    if (getActiveResourceBundle().usingRawMode()) {
      assertEquals(messageIdWithArg.toLocalizedString(missingArgs), englishMessageWithArgMissing);
    } else {
      assertEquals(messageIdWithArg.toLocalizedString(missingArgs), japaneseMessageWithArgMissing);
    }
  }

  @Test
  public void testEnglishLanguage() {
    StringId.setLocale(Locale.ENGLISH);
    assertEquals(messageId.toLocalizedString(), englishMessage);
  }

  @Test
  public void testJapaneseLanguage() {
    StringId.setLocale(Locale.JAPANESE);
    if (getActiveResourceBundle().usingRawMode()) {
      assertEquals(messageId.toLocalizedString(), englishMessage);
    } else {
      assertEquals(messageId.toLocalizedString(), japaneseMessage);
    }
  }

  @Test
  public void testAlternateEnglishCountries() {
    StringId.setLocale(Locale.CANADA);
    assertEquals(messageId.toLocalizedString(), englishMessage);

    StringId.setLocale(Locale.UK);
    assertEquals(messageId.toLocalizedString(), englishMessage);
  }

  /**
   * Use reflection to find all instances of StringId defined within the classes listed in
   * StringIdDefiningClasses and verify there are not any duplicate indexes used.
   */
  @Test
  public void testVerifyStringIdsAreUnique() {
    final Set<Integer> allStringIds = new HashSet<Integer>(3000);

    // Save all duplicate ids and report them at the end
    final Set<StringId> duplicates = new HashSet<StringId>();

    for (StringId instance : getAllStringIds()) {
      boolean isUnique = allStringIds.add(((StringId) instance).id);
      // Duplicate ids between 0-1023 are allowed since they are duplicated
      // between String bundles to minimize compiler dependencies.
      if ((!isUnique) && ((StringId) instance).id >= 1024) {
        boolean status = duplicates.add(instance);
        assertTrue("Failed to add " + instance + "to the list of"
            + " duplicates because of duplicate duplicates", status);
      }
    }
    if (!duplicates.isEmpty()) {
      StringBuilder err = new StringBuilder();
      err.append("The following duplicate StringIds were found:");
      for (StringId i : duplicates) {
        err.append("\n").append(((StringId) i).id).append(" : ").append(i.getRawText());
      }
      fail(err.toString() + "\nSearched in " + getStringIdDefiningClasses());
    }
  }

  /**
   * Verify the "Formated" form of the string matches the raw text form. This test is to catch
   * issues like bug #40146 This tests the English version in the US locale.
   */
  @Test
  public void testVerifyStringsAreProperlyEscaped_US() {
    verifyStringsAreProperlyEscaped(Locale.US);
  }

  /**
   * Verify the "Formated" form of the string matches the raw text form. This test is to catch
   * issues like bug #40146 This tests the English version in the UK locale.
   */
  @Test
  public void testVerifyStringsAreProperlyEscaped_UK() {
    verifyStringsAreProperlyEscaped(Locale.UK);
  }

  /**
   * Verify the "Formated" form of the string matches the raw text form. This test is to catch
   * issues like bug #40146 This tests the English version in the JAPAN locale.
   */
  @Test
  public void testVerifyStringsAreProperlyEscaped_JAPAN() {
    verifyStringsAreProperlyEscaped(Locale.JAPAN);
  }

  /**
   * Get all of the StringId instances via reflection.
   * 
   * @return a set of all StringId declarations within the product
   */
  private Set<StringId> getAllStringIds() {
    final Set<StringId> allStringIds = new HashSet<StringId>();
    for (String className : getStringIdDefiningClasses()) {
      try {
        Class<?> c = Class.forName(className);
        Field[] fields = c.getDeclaredFields();
        final String msg = "Found no StringIds in " + className;
        assertTrue(msg, fields.length > 0);
        for (Field f : fields) {
          f.setAccessible(true);
          StringId instance = (StringId) f.get(null);
          allStringIds.add(instance);
        }
      } catch (ClassNotFoundException cnfe) {
        throw new AssertionError(cnfe.toString(), cnfe);
      } catch (Exception e) {
        String exMsg =
            "Reflection attempt failed while attempting to find all" + " StringId instances. ";
        throw new AssertionError(exMsg + e.toString(), e);
      }
    }
    return allStringIds;
  }

  /**
   * Lists all of the classes that define StringId instances
   * 
   * @return a set of all classes that contain StringId declarations
   */
  private Set<String> getStringIdDefiningClasses() {
    final Set<String> StringIdDefiningClasses = new LinkedHashSet<String>();
    final String pkg = "org.apache.geode.internal.i18n.";
    // StringIdDefiningClasses.add(pkg + "ParentLocalizedStrings");
    StringIdDefiningClasses.add(pkg + "LocalizedStrings");
    StringIdDefiningClasses.add("org.apache.geode.management.internal.ManagementStrings");
    return StringIdDefiningClasses;
  }

  /**
   * Helper to access a private method via reflection, thus allowing us to not expose them to
   * customers.
   */
  private Locale getCurrentLocale() {
    Class<?> c = StringId.class;
    Locale locale = null;
    try {
      Method m = c.getDeclaredMethod("getCurrentLocale");
      m.setAccessible(true);
      locale = (Locale) m.invoke(null);
    } catch (Exception e) {
      String msg = "Reflection attempt failed for StringId.getCurrentLocale ";
      throw new AssertionError(msg + e.toString(), e);
    }
    return locale;
  }

  /**
   * Helper to access a private method via reflection, thus allowing us to not expose them to
   * customers.
   */
  private AbstractStringIdResourceBundle getActiveResourceBundle() {
    Class<?> c = StringId.class;
    AbstractStringIdResourceBundle rb = null;
    try {
      Method m = c.getDeclaredMethod("getActiveResourceBundle");
      m.setAccessible(true);
      rb = (AbstractStringIdResourceBundle) m.invoke(null);
    } catch (Exception e) {
      String msg = "Reflection attempt failed for StringId.getActiveResourceBundle ";
      throw new AssertionError(msg + e.toString(), e);
    }
    return rb;
  }

  /**
   * Check that the "raw" string matches the "formatted" string after taking into account known
   * changes due to the fomatting process. For example: <code>
   * "I''m using a contraction." should become "I'm using a contraction."
   * </code>
   */
  private void verifyStringsAreProperlyEscaped(Locale loc) {
    StringId.setLocale(loc);

    final Set<StringId> misquoted = new HashSet<StringId>();

    final Object[] identityArgs = new Object[100];

    for (int index = 0; index < identityArgs.length; index++) {
      identityArgs[index] = index;
    }

    final AbstractStringIdResourceBundle rb = getActiveResourceBundle();
    for (StringId instance : getAllStringIds()) {
      String raw = rb.getString(instance);
      String altered = raw.replaceAll("''", "'");
      altered = altered.replaceAll("\\{([0-9]+)[^\\}]*\\}", "$1");
      if (!rb.usingRawMode()) {
        altered = "msgID " + ((StringId) instance).id + ": " + altered;
      }
      String formatted = null;
      try {
        formatted = instance.toLocalizedString(identityArgs);
      } catch (IllegalArgumentException iae) {
        String testName = this.getClass().getName().replaceAll("\\.", "/") + ".class";
        String exMsg = "Improper message id=" + ((StringId) instance).id + "\n"
            + "Usually this is caused by an unmatched or nested \"{\"\n"
            + "Examples:\t\"{0]\" or \"{ {0} }\"\n"
            + "This is just the first failure, it is in your interest"
            + " to rebuild and run just this one test.\n"
            + "build.sh run-java-tests -Djunit.testcase=" + testName;
        throw new AssertionError(exMsg, iae);
      }
      if (!altered.equals(formatted)) {
        System.err.println("altered:   " + altered);
        System.err.println("formatted: " + formatted);
        misquoted.add(instance);
      }
    }
    if (!misquoted.isEmpty()) {
      StringBuffer err = new StringBuffer();
      err.append("These errors are usually resolved by replacing ");
      err.append("\"'\" with \"''\".\n");
      err.append("If the error is in the non-english version then ");
      err.append("alter the text in StringIdResouceBundle_{lang}.txt.\n");
      err.append("The following misquoted StringIds were found:");
      for (StringId i : misquoted) {
        err.append("\n").append("StringId id=").append(((StringId) i).id).append(" : text=\"")
            .append(i.getRawText()).append("\"");
      }
      fail(err.toString());
    }
  }
}
