package morfologik.speller;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;

import morfologik.stemming.Dictionary;

import org.assertj.core.api.Assertions;
import org.junit.BeforeClass;
import org.junit.Test;

public class SpellerTest {
  private static Dictionary dictionary;

  @BeforeClass
  public static void setup() throws Exception {
    final URL url = SpellerTest.class.getResource("slownik.dict");
    dictionary = Dictionary.read(url);
  }

  /*
    @Test
    public void testAbka() throws Exception {
        final Speller spell = new Speller(dictionary, 2);
        System.out.println("Replacements:");
        for (String s : spell.findReplacements("abka")) {
            System.out.println(s);
        }
    }
   */

  @Test
  public void testRunonWords() throws IOException {
    final Speller spell = new Speller(dictionary);
    Assertions.assertThat(spell.replaceRunOnWords("abaka")).isEmpty();
    Assertions.assertThat(spell.replaceRunOnWords("abakaabace")).contains("abaka abace");
    Assertions.assertThat(spell.replaceRunOnWords("Abakaabace")).contains("Abaka abace");
    Assertions.assertThat(spell.replaceRunOnWords("AbakaAbace")).contains("Abaka Abace");
    Assertions.assertThat(spell.replaceRunOnWords("abakaAbace")).contains("abaka Abace");


    // Test on an morphological dictionary - should work as well
    final URL url1 = getClass().getResource("test-infix.dict");
    final Speller spell1 = new Speller(Dictionary.read(url1));
    assertTrue(spell1.replaceRunOnWords("Rzekunia").isEmpty());
    assertTrue(spell1.replaceRunOnWords("RzekuniaRzeczypospolitej").contains("Rzekunia Rzeczypospolitej"));
    assertTrue(spell1.replaceRunOnWords("RzekuniaRze").isEmpty()); //Rze is not found but is a prefix

    final URL url2 = getClass().getResource("single-char-word.dict");
    final Speller spell2 = new Speller(Dictionary.read(url2));
    assertTrue(spell2.replaceRunOnWords("alot").contains("a lot"));
    assertTrue(spell2.replaceRunOnWords("Alot").contains("A lot"));
    assertTrue(spell2.replaceRunOnWords("ALot").contains("A Lot"));
    assertTrue(spell2.replaceRunOnWords("LotAmusement").contains("Lot Amusement"));
    //TODO? assertTrue(spell2.replaceRunOnWords("LOTAMUSEMENT").contains("LOT AMUSEMENT"));
    assertTrue(spell2.replaceRunOnWords("aalot").contains("aa lot"));
    assertTrue(spell2.replaceRunOnWords("aamusement").contains("a amusement"));
    assertTrue(spell2.replaceRunOnWords("clot").isEmpty());
    assertTrue(spell2.replaceRunOnWords("foobar").isEmpty());
  }

  @Test
  public void testIsInDictionary() throws IOException {
    // Test on an morphological dictionary, including separators
    final URL url1 = getClass().getResource("test-infix.dict");
    final Speller spell1 = new Speller(Dictionary.read(url1));
    assertTrue(spell1.isInDictionary("Rzekunia"));
    assertTrue(!spell1.isInDictionary("Rzekunia+"));
    assertTrue(!spell1.isInDictionary("Rzekunia+aaa"));
    // test UTF-8 dictionary
    final URL url = getClass().getResource("test-utf-spell.dict");
    final Speller spell = new Speller(Dictionary.read(url));
    assertTrue(spell.isInDictionary("jaźń"));
    assertTrue(spell.isInDictionary("zażółć"));
    assertTrue(spell.isInDictionary("żółwiową"));
    assertTrue(spell.isInDictionary("ćwikła"));
    assertTrue(spell.isInDictionary("Żebrowski"));
    assertTrue(spell.isInDictionary("Święto"));
    assertTrue(spell.isInDictionary("Świerczewski"));
    assertTrue(spell.isInDictionary("abc"));
  }

  @Test
  public void testFindReplacements() throws IOException {
    final Speller spell = new Speller(dictionary, 1);
    assertTrue(spell.findReplacements("abka").contains("abak"));
    //check if we get only dictionary words...
    List<String> reps = spell.findReplacements("bak");
    for (final String word: reps) {
      assertTrue(spell.isInDictionary(word));
    }
    assertTrue(spell.findReplacements("abka~~").isEmpty()); // 2 characters more -> edit distance too large
    assertTrue(!spell.findReplacements("Rezkunia").contains("Rzekunia"));

    final URL url1 = getClass().getResource("test-infix.dict");
    final Speller spell1 = new Speller(Dictionary.read(url1));
    assertTrue(spell1.findReplacements("Rezkunia").contains("Rzekunia"));
    //diacritics
    assertTrue(spell1.findReplacements("Rzękunia").contains("Rzekunia"));
    //we should get no candidates for correct words
    assertTrue(spell1.isInDictionary("Rzekunia"));
    assertTrue(spell1.findReplacements("Rzekunia").isEmpty());
    //and no for things that are too different from the dictionary
    assertTrue(spell1.findReplacements("Strefakibica").isEmpty());
    //nothing for nothing
    assertTrue(spell1.findReplacements("").isEmpty());
    //nothing for weird characters
    assertTrue(spell1.findReplacements("\u0000").isEmpty());
    //nothing for other characters
    assertTrue(spell1.findReplacements("«…»").isEmpty());
    //nothing for separator
    assertTrue(spell1.findReplacements("+").isEmpty());

  }

  @Test
  public void testFrequencyNonUTFDictionary() throws IOException {
    final URL url1 = getClass().getResource("test_freq_iso.dict");
    final Speller spell = new Speller(Dictionary.read(url1));
    assertTrue(spell.isInDictionary("a"));
    assertTrue(!spell.isInDictionary("aõh")); //non-encodable in UTF-8
  }

  @Test
  public void testFindReplacementsInUTF() throws IOException {
    final URL url = getClass().getResource("test-utf-spell.dict");
    final Speller spell = new Speller(Dictionary.read(url));
    assertTrue(spell.findReplacements("gęslą").contains("gęślą"));
    assertTrue(spell.findReplacements("ćwikla").contains("ćwikła"));
    assertTrue(spell.findReplacements("Swierczewski").contains("Świerczewski"));
    assertTrue(spell.findReplacements("zółwiową").contains("żółwiową"));
    assertTrue(spell.findReplacements("Żebrowsk").contains("Żebrowski"));
    assertTrue(spell.findReplacements("święto").contains("Święto"));
    //note: no diacritics here, but we still get matches!
    assertTrue(spell.findReplacements("gesla").contains("gęślą"));
    assertTrue(spell.findReplacements("swieto").contains("Święto"));
    assertTrue(spell.findReplacements("zolwiowa").contains("żółwiową"));
    //using equivalent characters 'x' = 'ź'
    assertTrue(spell.findReplacements("jexn").contains("jaźń"));
    // 'u' = 'ó', so the edit distance is still small...
    assertTrue(spell.findReplacements("zażulv").contains("zażółć"));
    // 'rz' = 'ż', so the edit distance is still small, but with string replacements...
    assertTrue(spell.findReplacements("zarzulv").contains("zażółć"));
    assertTrue(spell.findReplacements("Rzebrowski").contains("Żebrowski"));
    assertTrue(spell.findReplacements("rzółw").contains("żółw"));
    assertTrue(spell.findReplacements("Świento").contains("Święto"));
    // avoid mixed-case words as suggestions when using replacements ('rz' = 'ż')
    assertTrue(spell.findReplacements("zArzółć").get(0).equals("zażółć"));
  }

  @Test
  public void testFindReplacementsUsingFrequency() throws IOException {
    final URL url = getClass().getResource("dict-with-freq.dict");
    final Speller spell = new Speller(Dictionary.read(url));

    //check if we get only dictionary words...
    List<String> reps = spell.findReplacements("jist");
    for (final String word: reps) {
      assertTrue(spell.isInDictionary(word));
    }
    // get replacements ordered by frequency
    assertTrue(reps.get(0).equals("just"));
    assertTrue(reps.get(1).equals("list"));
    assertTrue(reps.get(2).equals("fist"));
    assertTrue(reps.get(3).equals("mist"));
    assertTrue(reps.get(4).equals("jest"));
    assertTrue(reps.get(5).equals("dist"));
    assertTrue(reps.get(6).equals("gist"));
  }
  
  @Test
  public void testFindSimilarWords() throws IOException {
    final URL url = getClass().getResource("dict-with-freq.dict");
    final Speller spell = new Speller(Dictionary.read(url));

    List<String> reps = spell.findSimilarWords("fist");
    assertTrue(reps.toString().equals("[list, mist, dist, gist, wist, hist]"));
    reps = spell.findSimilarWords("mist");
    assertTrue(reps.toString().equals("[list, fist, dist, gist, wist, hist]"));
    reps = spell.findSimilarWords("Fist");
    assertTrue(reps.toString().equals("[fist, list, mist, dist, gist, wist, hist]"));
    reps = spell.findSimilarWords("licit");
    assertTrue(reps.toString().equals("[list, fist, mist, dist, gist, wist, hist]"));
  }

  @Test
  public void testConcurrentReplacements() throws IOException {
    final URL url = getClass().getResource("dict-with-freq.dict");
    final Speller spell = new Speller(Dictionary.read(url));
    //only the longest key is selected in replacement pairs
    List<String> reps = spell.getAllReplacements("teached", 0, 0);
    assertTrue(reps.contains("teached"));
    assertTrue(reps.contains("taught"));
    assertTrue(!reps.contains("tgheached"));
  }
  

  @Test
  public void testIsMisspelled() throws IOException {
    final URL url = getClass().getResource("test-utf-spell.dict");
    final Speller spell = new Speller(Dictionary.read(url));
    assertTrue(!spell.isMisspelled("Paragraf22"));  //ignorujemy liczby
    assertTrue(!spell.isMisspelled("!"));  //ignorujemy znaki przestankowe
    assertTrue(spell.isMisspelled("dziekie"));  //test, czy znajdujemy błąd
    assertTrue(!spell.isMisspelled("SłowozGarbem"));  //ignorujemy słowa w stylu wielbłąda
    assertTrue(!spell.isMisspelled("Ćwikła"));  //i małe litery
    assertTrue(!spell.isMisspelled("TOJESTTEST"));  //i wielkie litery
    final Speller oldStyleSpell = new Speller(dictionary, 1);
    assertTrue(oldStyleSpell.isMisspelled("Paragraf22"));  // nie ignorujemy liczby
    assertTrue(oldStyleSpell.isMisspelled("!"));  //nie ignorujemy znaków przestankowych
    // assertTrue(oldStyleSpell.isMisspelled("SłowozGarbem"));  //ignorujemy słowa w stylu wielbłąda
    assertTrue(oldStyleSpell.isMisspelled("Abaka"));  //i małe litery
    final URL url1 = getClass().getResource("test-infix.dict");
    final Speller spell1 = new Speller(Dictionary.read(url1));
    assertTrue(!spell1.isMisspelled("Rzekunia"));
    assertTrue(spell1.isAllUppercase("RZEKUNIA"));
    assertTrue(spell1.isMisspelled("RZEKUNIAA")); // finds a typo here
    assertTrue(!spell1.isMisspelled("RZEKUNIA")); // but not here
  }

  @Test
  public void testCamelCase() {
    final Speller spell = new Speller(dictionary, 1);
    assertTrue(spell.isCamelCase("CamelCase"));
    assertTrue(!spell.isCamelCase("Camel"));
    assertTrue(!spell.isCamelCase("CAMEL"));
    assertTrue(!spell.isCamelCase("camel"));
    assertTrue(!spell.isCamelCase("cAmel"));
    assertTrue(!spell.isCamelCase("CAmel"));
    assertTrue(!spell.isCamelCase(""));
    assertTrue(!spell.isCamelCase(null));
  }
  
  @Test
  public void testCapitalizedWord() {
    final Speller spell = new Speller(dictionary, 1);
    assertTrue(spell.isNotCapitalizedWord("CamelCase"));
    assertTrue(!spell.isNotCapitalizedWord("Camel"));
    assertTrue(spell.isNotCapitalizedWord("CAMEL"));
    assertTrue(spell.isNotCapitalizedWord("camel"));
    assertTrue(spell.isNotCapitalizedWord("cAmel"));
    assertTrue(spell.isNotCapitalizedWord("CAmel"));
    assertTrue(spell.isNotCapitalizedWord(""));
  }

  @Test
  public void testGetAllReplacements() throws IOException {
    final URL url = getClass().getResource("test-utf-spell.dict");
    final Speller spell = new Speller(Dictionary.read(url));
    assertTrue(spell.isMisspelled("rzarzerzarzu"));
    assertEquals("[rzarzerzarzu]",
        Arrays.toString(spell.getAllReplacements("rzarzerzarzu", 0, 0).toArray()));
  }

  @Test
  public void testEditDistanceCalculation() throws IOException {
    final Speller spell = new Speller(dictionary, 5);
    //test examples from Oflazer's paper
    assertTrue(getEditDistance(spell, "recoginze", "recognize") == 1);
    assertTrue(getEditDistance(spell, "sailn", "failing") == 3);
    assertTrue(getEditDistance(spell, "abc", "abcd") == 1);
    assertTrue(getEditDistance(spell, "abc", "abcde") == 2);
    //test words from fsa_spell output
    assertTrue(getEditDistance(spell, "abka", "abaka") == 1);
    assertTrue(getEditDistance(spell, "abka", "abakan") == 2);
    assertTrue(getEditDistance(spell, "abka", "abaką") == 2);
    assertTrue(getEditDistance(spell, "abka", "abaki") == 2);
  }

  @Test
  public void testCutOffEditDistance() throws IOException {
    final Speller spell2 = new Speller(dictionary, 2); //note: threshold = 2
    //test cut edit distance - reprter / repo from Oflazer
    assertTrue(getCutOffDistance(spell2, "repo", "reprter") == 1);
    assertTrue(getCutOffDistance(spell2, "reporter", "reporter") == 0);
  }

  @Test
  public void testReplacementsAndDistance2() throws Exception {
    /*File infoFile = new File("/tmp/morfologik.info");
    FileWriter fw1 = new FileWriter(infoFile);
    fw1.write("fsa.dict.separator=+\n");
    fw1.write("fsa.dict.encoding=utf-8\n");
    fw1.write("fsa.dict.speller.replacement-pairs=s ss,t d,R Rh,y ij,ę em,em ę\n");
    fw1.close();

    File inputFile = new File("/tmp/morfologik.txt");
    FileWriter fw2 = new FileWriter(inputFile);
    fw2.write("Mitmuss\n");
    fw2.write("Rhythmus\n");
    fw2.write("Wald\n");
    fw2.write("Band\n");
    fw2.write("ijo\n");
    fw2.write("ijond\n");
    fw2.write("youd\n");
    fw2.write("ijoussud\n");
    fw2.write("ijoussuud\n");
    fw2.write("ijussuud\n");
    fw2.write("ijousod\n");
    fw2.write("ij\n");
    fw2.write("ijo\n");
    fw2.write("Ciarkę\n");
    fw2.write("Czarkę\n");
    fw2.write("Clarke\n");
    fw2.write("Clarkiem\n");
    fw2.write("Clarkom\n");
    
    fw2.close();

    File dictFile = new File("/tmp/morfologik.dict");
    String[] buildToolOptions =
            {"-i", inputFile.getAbsolutePath(), "-o", dictFile.getAbsolutePath()};
    FSABuildTool.main(buildToolOptions);
    Dictionary dictionary = Dictionary.read(dictFile);
    Speller speller = new Speller(dictionary, 3);*/
    
    final URL url = getClass().getResource("reps_dist2.dict");
    final Speller speller = new Speller(Dictionary.read(url), 3);
    
    List<String> reps = speller.findReplacements("Rytmus");
    assertTrue(reps.get(0).equals("Rhythmus"));
    assertTrue(reps.get(1).equals("Mitmuss"));
    reps = speller.findReplacements("Walt");
    assertTrue(reps.get(0).equals("Wald"));
    assertTrue(reps.get(1).equals("Band"));
    reps = speller.findReplacements("yout");
    assertTrue(reps.get(0).equals("youd"));
    assertTrue(reps.get(1).equals("ijond"));
    assertTrue(reps.get(2).equals("ijo"));
    reps = speller.findReplacements("yousut");
    assertTrue(reps.get(0).equals("ijoussud"));
    assertTrue(reps.get(1).equals("ijousod"));
    assertTrue(reps.get(2).equals("ijoussuud"));
    assertTrue(reps.get(3).equals("youd"));
    reps = speller.findReplacements("yo");
    assertTrue(reps.get(0).equals("ijo"));
    assertTrue(reps.get(1).equals("ij"));
    reps = speller.findReplacements("Clarkem");
    assertTrue(reps.get(0).equals("Ciarkę"));
    assertTrue(reps.get(1).equals("Clarke"));
    assertTrue(reps.get(2).equals("Clarkiem"));
    assertTrue(reps.get(3).equals("Clarkom"));
    assertTrue(reps.get(4).equals("Czarkę"));
  }

  @Test
  public void testIssue94() throws Exception {
    final URL url = getClass().getResource("issue94.dict");
    final Speller speller = new Speller(Dictionary.read(url));
    List<String> reps = speller.findReplacements("schänken");
    assertTrue(reps.get(0).equals("Schänken"));
    assertTrue(reps.get(1).equals("schenken"));
  }

  private int getCutOffDistance(final Speller spell, final String word, final String candidate) {
    // assuming there is no pair-replacement 
    spell.setWordAndCandidate(word, candidate);
    final int [] ced = new int[spell.getCandLen() - spell.getWordLen()];
    for (int i = 0; i < spell.getCandLen() - spell.getWordLen(); i++) {
      ced[i] = spell.cuted(spell.getWordLen() + i, spell.getWordLen() + i, spell.getWordLen() + i);
    }
    Arrays.sort(ced);
    //and the min value...
    if (ced.length > 0) {
      return ced[0];
    }
    return 0;
  }

  private int getEditDistance(final Speller spell, final String word, final String candidate) {
    // assuming there is no pair-replacement
    spell.setWordAndCandidate(word, candidate);
    final int maxDistance = spell.getEffectiveED();
    final int candidateLen = spell.getCandLen();
    final int wordLen = spell.getWordLen();
    int ed = 0;
    for (int i = 0; i < candidateLen; i++) {
      if (spell.cuted(i, i, i) <= maxDistance) {
        if (Math.abs(wordLen - 1 - i) <= maxDistance) {
          ed = spell.ed(wordLen - 1, i, wordLen - 1, i);
        }
      }
    }
    return ed;
  }
}