/*
 * 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.solr.internal.csv;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Arrays;

import junit.framework.TestCase;

/**
 * CSVParserTest
 *
 * The test are organized in three different sections:
 * The 'setter/getter' section, the lexer section and finally the parser 
 * section. In case a test fails, you should follow a top-down approach for 
 * fixing a potential bug (it's likely that the parser itself fails if the lexer
 * has problems...).
 */
public class CSVParserTest extends TestCase {
  
  /**
   * TestCSVParser.
   */
  class TestCSVParser extends CSVParser {
    /**
     * Test parser to investigate the type of the internal Token.
     * @param in a Reader
     */
    TestCSVParser(Reader in) {
      super(in);
    }

    TestCSVParser(Reader in, CSVStrategy strategy) {
      super(in, strategy);
    }
    /**
     * Calls super.nextToken() and prints out a String representation of token
     * type and content.
     * @return String representation of token type and content
     * @throws IOException like {@link CSVParser#nextToken()}
     */
    public String testNextToken() throws IOException {
      Token t = super.nextToken();
      return Integer.toString(t.type) + ";" + t.content + ";";
    }
  }
  
  // ======================================================
  //   lexer tests
  // ======================================================
  
  // Single line (without comment)
  public void testNextToken1() throws IOException {
    String code = "abc,def, hijk,  lmnop,   qrst,uv ,wxy   ,z , ,";
    TestCSVParser parser = new TestCSVParser(new StringReader(code));
    assertEquals(CSVParser.TT_TOKEN + ";abc;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";def;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";hijk;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";lmnop;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";qrst;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";uv;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";wxy;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";z;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";;", parser.testNextToken());
    assertEquals(CSVParser.TT_EOF + ";;", parser.testNextToken());  
  }
  
  // multiline including comments (and empty lines)
  public void testNextToken2() throws IOException {
    /*   file:   1,2,3,
     *           a,b x,c
     *
     *           # this is a comment 
     *           d,e,
     * 
     */
    String code = "1,2,3,\na,b x,c\n#foo\n\nd,e,\n\n";
    CSVStrategy strategy = (CSVStrategy)CSVStrategy.DEFAULT_STRATEGY.clone();
    // strategy.setIgnoreEmptyLines(false);
    strategy.setCommentStart('#');

    TestCSVParser parser = new TestCSVParser(new StringReader(code), strategy);


    assertEquals(CSVParser.TT_TOKEN + ";1;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";2;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";3;", parser.testNextToken());
    assertEquals(CSVParser.TT_EORECORD + ";;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";a;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";b x;", parser.testNextToken());
    assertEquals(CSVParser.TT_EORECORD + ";c;", parser.testNextToken());
    assertEquals(CSVParser.TT_EORECORD + ";;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";d;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";e;", parser.testNextToken());
    assertEquals(CSVParser.TT_EORECORD + ";;", parser.testNextToken());
    assertEquals(CSVParser.TT_EOF + ";;", parser.testNextToken());    
    assertEquals(CSVParser.TT_EOF + ";;", parser.testNextToken());    
    
  }
 
  // simple token with escaping
  public void testNextToken3() throws IOException {
    /* file: a,\,,b
     *       \,,
     */
    String code = "a,\\,,b\n\\,,";
    CSVStrategy strategy = (CSVStrategy)CSVStrategy.DEFAULT_STRATEGY.clone();
    strategy.setCommentStart('#');
    TestCSVParser parser = new TestCSVParser(new StringReader(code), strategy);

    assertEquals(CSVParser.TT_TOKEN + ";a;", parser.testNextToken());
    // an unquoted single backslash is not an escape char
    assertEquals(CSVParser.TT_TOKEN + ";\\;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";;", parser.testNextToken());
    assertEquals(CSVParser.TT_EORECORD + ";b;", parser.testNextToken());
    // an unquoted single backslash is not an escape char
    assertEquals(CSVParser.TT_TOKEN + ";\\;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";;", parser.testNextToken());
    assertEquals(CSVParser.TT_EOF + ";;", parser.testNextToken());
  }
  
  // encapsulator tokenizer (sinle line)
  public void testNextToken4() throws IOException {
    /* file:  a,"foo",b
     *        a,   " foo",b
     *        a,"foo "   ,b     // whitespace after closing encapsulator
     *        a,  " foo " ,b
     */ 
     String code = 
      "a,\"foo\",b\na,   \" foo\",b\na,\"foo \"  ,b\na,  \" foo \"  ,b";
     TestCSVParser parser = new TestCSVParser(new StringReader(code));
     assertEquals(CSVParser.TT_TOKEN + ";a;", parser.testNextToken());
     assertEquals(CSVParser.TT_TOKEN + ";foo;", parser.testNextToken());
     assertEquals(CSVParser.TT_EORECORD + ";b;", parser.testNextToken());
     assertEquals(CSVParser.TT_TOKEN + ";a;", parser.testNextToken());
     assertEquals(CSVParser.TT_TOKEN + "; foo;", parser.testNextToken());
     assertEquals(CSVParser.TT_EORECORD + ";b;", parser.testNextToken());
     assertEquals(CSVParser.TT_TOKEN + ";a;", parser.testNextToken());
     assertEquals(CSVParser.TT_TOKEN + ";foo ;", parser.testNextToken());
     assertEquals(CSVParser.TT_EORECORD + ";b;", parser.testNextToken());
     assertEquals(CSVParser.TT_TOKEN + ";a;", parser.testNextToken());
     assertEquals(CSVParser.TT_TOKEN + "; foo ;", parser.testNextToken());
//     assertEquals(CSVParser.TT_EORECORD + ";b;", parser.testNextToken());
     assertEquals(CSVParser.TT_EOF + ";b;", parser.testNextToken());    
  }
  
  // encapsulator tokenizer (multi line, delimiter in string)
  public void testNextToken5() throws IOException {   
    String code = 
      "a,\"foo\n\",b\n\"foo\n  baar ,,,\"\n\"\n\t \n\"";
    TestCSVParser parser = new TestCSVParser(new StringReader(code));
    assertEquals(CSVParser.TT_TOKEN + ";a;", parser.testNextToken());
    assertEquals(CSVParser.TT_TOKEN + ";foo\n;", parser.testNextToken());
    assertEquals(CSVParser.TT_EORECORD + ";b;", parser.testNextToken());
    assertEquals(CSVParser.TT_EORECORD + ";foo\n  baar ,,,;",
        parser.testNextToken());
    assertEquals(CSVParser.TT_EOF + ";\n\t \n;", parser.testNextToken());

  }
  
  // change delimiters, comment, encapsulater
  public void testNextToken6() throws IOException {
    /* file: a;'b and \' more
     *       '
     *       !comment;;;;
     *       ;;
     */
    String code = "a;'b and '' more\n'\n!comment;;;;\n;;";
    TestCSVParser parser = new TestCSVParser(new StringReader(code), new CSVStrategy(';', '\'', '!'));
    assertEquals(CSVParser.TT_TOKEN + ";a;", parser.testNextToken());
    assertEquals(
      CSVParser.TT_EORECORD + ";b and ' more\n;", 
      parser.testNextToken());
  }
  
  
  // ======================================================
  //   parser tests
  // ======================================================
  
  String code = 
    "a,b,c,d\n"
    + " a , b , 1 2 \n"
    + "\"foo baar\", b,\n"
   // + "   \"foo\n,,\n\"\",,\n\\\"\",d,e\n";
      + "   \"foo\n,,\n\"\",,\n\"\"\",d,e\n";   // changed to use standard CSV escaping
  String[][] res = {
    {"a", "b", "c", "d"},
    {"a", "b", "1 2"}, 
    {"foo baar", "b", ""}, 
    {"foo\n,,\n\",,\n\"", "d", "e"}
  };
  public void testGetLine() throws IOException {
    CSVParser parser = new CSVParser(new StringReader(code));
    String[] tmp = null;
    for (int i = 0; i < res.length; i++) {
      tmp = parser.getLine();
      assertTrue(Arrays.equals(res[i], tmp));
    }
    tmp = parser.getLine();
    assertTrue(tmp == null);
  }
  
  public void testNextValue() throws IOException {
    CSVParser parser = new CSVParser(new StringReader(code));
    String tmp = null;
    for (int i = 0; i < res.length; i++) {
      for (int j = 0; j < res[i].length; j++) {
        tmp = parser.nextValue();
        assertEquals(res[i][j], tmp);
      }
    }
    tmp = parser.nextValue();
    assertTrue(tmp == null);    
  }
  
  public void testGetAllValues() throws IOException {
    CSVParser parser = new CSVParser(new StringReader(code));
    String[][] tmp = parser.getAllValues();
    assertEquals(res.length, tmp.length);
    assertTrue(tmp.length > 0);
    for (int i = 0; i < res.length; i++) {
      assertTrue(Arrays.equals(res[i], tmp[i])); 
    }
  }
  
  public void testExcelStrategy1() throws IOException {
    String code = 
      "value1,value2,value3,value4\r\na,b,c,d\r\n  x,,,"
      + "\r\n\r\n\"\"\"hello\"\"\",\"  \"\"world\"\"\",\"abc\ndef\",\r\n";
    String[][] res = {
      {"value1", "value2", "value3", "value4"},
      {"a", "b", "c", "d"},
      {"  x", "", "", ""},
      {""},
      {"\"hello\"", "  \"world\"", "abc\ndef", ""}
    };
    CSVParser parser = new CSVParser(new StringReader(code), CSVStrategy.EXCEL_STRATEGY);
    String[][] tmp = parser.getAllValues();
    assertEquals(res.length, tmp.length);
    assertTrue(tmp.length > 0);
    for (int i = 0; i < res.length; i++) {
      assertTrue(Arrays.equals(res[i], tmp[i])); 
    }
  }
  
  public void testExcelStrategy2() throws Exception {
    String code = "foo,baar\r\n\r\nhello,\r\n\r\nworld,\r\n";
    String[][] res = {
      {"foo", "baar"},
      {""},
      {"hello", ""},
      {""},
      {"world", ""}
    };
    CSVParser parser = new CSVParser(new StringReader(code), CSVStrategy.EXCEL_STRATEGY);
    String[][] tmp = parser.getAllValues();
    assertEquals(res.length, tmp.length);
    assertTrue(tmp.length > 0);
    for (int i = 0; i < res.length; i++) {
      assertTrue(Arrays.equals(res[i], tmp[i])); 
    }
  }
  
  public void testEndOfFileBehaviourExcel() throws Exception {
    String[] codes = {
        "hello,\r\n\r\nworld,\r\n",
        "hello,\r\n\r\nworld,",
        "hello,\r\n\r\nworld,\"\"\r\n",
        "hello,\r\n\r\nworld,\"\"",
        "hello,\r\n\r\nworld,\n",
        "hello,\r\n\r\nworld,",
        "hello,\r\n\r\nworld,\"\"\n",
        "hello,\r\n\r\nworld,\"\""
        };
    String[][] res = {
      {"hello", ""},
      {""},  // ExcelStrategy does not ignore empty lines
      {"world", ""}
    };
    String code;
    for (int codeIndex = 0; codeIndex < codes.length; codeIndex++) {
      code = codes[codeIndex];
      CSVParser parser = new CSVParser(new StringReader(code), CSVStrategy.EXCEL_STRATEGY);
      String[][] tmp = parser.getAllValues();
      assertEquals(res.length, tmp.length);
      assertTrue(tmp.length > 0);
      for (int i = 0; i < res.length; i++) {
        assertTrue(Arrays.equals(res[i], tmp[i]));
      }
    }
  }
  
  public void testEndOfFileBehaviorCSV() throws Exception {
    String[] codes = {
        "hello,\r\n\r\nworld,\r\n",
        "hello,\r\n\r\nworld,",
        "hello,\r\n\r\nworld,\"\"\r\n",
        "hello,\r\n\r\nworld,\"\"",
        "hello,\r\n\r\nworld,\n",
        "hello,\r\n\r\nworld,",
        "hello,\r\n\r\nworld,\"\"\n",
        "hello,\r\n\r\nworld,\"\""
        };
    String[][] res = {
      {"hello", ""},  // CSV Strategy ignores empty lines
      {"world", ""}
    };
    String code;
    for (int codeIndex = 0; codeIndex < codes.length; codeIndex++) {
      code = codes[codeIndex];
      CSVParser parser = new CSVParser(new StringReader(code));
      String[][] tmp = parser.getAllValues();
      assertEquals(res.length, tmp.length);
      assertTrue(tmp.length > 0);
      for (int i = 0; i < res.length; i++) {
        assertTrue(Arrays.equals(res[i], tmp[i]));
      }
    }
  }
  
  public void testEmptyLineBehaviourExcel() throws Exception {
    String[] codes = {
        "hello,\r\n\r\n\r\n",
        "hello,\n\n\n",
        "hello,\"\"\r\n\r\n\r\n",
        "hello,\"\"\n\n\n"
        };
    String[][] res = {
      {"hello", ""},
      {""},  // ExcelStrategy does not ignore empty lines
      {""}
    };
    String code;
    for (int codeIndex = 0; codeIndex < codes.length; codeIndex++) {
      code = codes[codeIndex];
      CSVParser parser = new CSVParser(new StringReader(code), CSVStrategy.EXCEL_STRATEGY);
      String[][] tmp = parser.getAllValues();
      assertEquals(res.length, tmp.length);
      assertTrue(tmp.length > 0);
      for (int i = 0; i < res.length; i++) {
        assertTrue(Arrays.equals(res[i], tmp[i]));
      }
    }
  }
  
  public void testEmptyLineBehaviourCSV() throws Exception {
    String[] codes = {
        "hello,\r\n\r\n\r\n",
        "hello,\n\n\n",
        "hello,\"\"\r\n\r\n\r\n",
        "hello,\"\"\n\n\n"
        };
    String[][] res = {
      {"hello", ""}  // CSV Strategy ignores empty lines
    };
    String code;
    for (int codeIndex = 0; codeIndex < codes.length; codeIndex++) {
      code = codes[codeIndex];
      CSVParser parser = new CSVParser(new StringReader(code));
      String[][] tmp = parser.getAllValues();
      assertEquals(res.length, tmp.length);
      assertTrue(tmp.length > 0);
      for (int i = 0; i < res.length; i++) {
        assertTrue(Arrays.equals(res[i], tmp[i]));
      }
    }
  }
  
  public void OLDtestBackslashEscaping() throws IOException {
    String code =
      "one,two,three\n"
      + "on\\\"e,two\n"
      + "on\"e,two\n"
      + "one,\"tw\\\"o\"\n"
      + "one,\"t\\,wo\"\n"
      + "one,two,\"th,ree\"\n"
      + "\"a\\\\\"\n"
      + "a\\,b\n"
      + "\"a\\\\,b\"";
    String[][] res = {
        { "one", "two", "three" },
        { "on\\\"e", "two" },
        { "on\"e", "two" },
        { "one", "tw\"o" },
        { "one", "t\\,wo" },  // backslash in quotes only escapes a delimiter (",")
        { "one", "two", "th,ree" },
        { "a\\\\" },     // backslash in quotes only escapes a delimiter (",")
        { "a\\", "b" },  // a backslash must be returnd 
        { "a\\\\,b" }    // backslash in quotes only escapes a delimiter (",")
      };
    CSVParser parser = new CSVParser(new StringReader(code));
    String[][] tmp = parser.getAllValues();
    assertEquals(res.length, tmp.length);
    assertTrue(tmp.length > 0);
    for (int i = 0; i < res.length; i++) {
      assertTrue(Arrays.equals(res[i], tmp[i])); 
    }
  }
  
  public void testBackslashEscaping() throws IOException {

    // To avoid confusion over the need for escaping chars in java code,
    // We will test with a forward slash as the escape char, and a single
    // quote as the encapsulator.

    String code =
      "one,two,three\n" // 0
      + "'',''\n"       // 1) empty encapsulators
      + "/',/'\n"       // 2) single encapsulators
      + "'/'','/''\n"   // 3) single encapsulators encapsulated via escape
      + "'''',''''\n"   // 4) single encapsulators encapsulated via doubling
      + "/,,/,\n"       // 5) separator escaped
      + "//,//\n"       // 6) escape escaped
      + "'//','//'\n"   // 7) escape escaped in encapsulation
      + "   8   ,   \"quoted \"\" /\" // string\"   \n"     // don't eat spaces
      + "9,   /\n   \n"  // escaped newline
      + "";
    String[][] res = {
        { "one", "two", "three" }, // 0
        { "", "" },                // 1
        { "'", "'" },              // 2
        { "'", "'" },              // 3
        { "'", "'" },              // 4
        { ",", "," },              // 5
        { "/", "/" },              // 6
        { "/", "/" },              // 7
        { "   8   ", "   \"quoted \"\" \" / string\"   " },
        { "9", "   \n   " },
      };


    CSVStrategy strategy = new CSVStrategy(',','\'',CSVStrategy.COMMENTS_DISABLED,'/',false,false,true,true);

    CSVParser parser = new CSVParser(new StringReader(code), strategy);
    String[][] tmp = parser.getAllValues();
    assertTrue(tmp.length > 0);
    for (int i = 0; i < res.length; i++) {
      assertTrue(Arrays.equals(res[i], tmp[i]));
    }
  }

  public void testBackslashEscaping2() throws IOException {

    // To avoid confusion over the need for escaping chars in java code,
    // We will test with a forward slash as the escape char, and a single
    // quote as the encapsulator.

    String code = ""
      + " , , \n"           // 1)
      + " \t ,  , \n"       // 2)
      + " // , /, , /,\n"   // 3)
      + "";
    String[][] res = {
        { " ", " ", " " },         // 1
        { " \t ", "  ", " " },         // 2
        { " / ", " , ", " ," },         //3
      };


    CSVStrategy strategy = new CSVStrategy(',',CSVStrategy.ENCAPSULATOR_DISABLED,CSVStrategy.COMMENTS_DISABLED,'/',false,false,true,true);

    CSVParser parser = new CSVParser(new StringReader(code), strategy);
    String[][] tmp = parser.getAllValues();
    assertTrue(tmp.length > 0);

    if (!CSVPrinterTest.equals(res, tmp)) {
      assertTrue(false);
    }

  }


  public void testDefaultStrategy() throws IOException {

    String code = ""
        + "a,b\n"            // 1)
        + "\"\n\",\" \"\n"   // 2)
        + "\"\",#\n"   // 2)
        ;
    String[][] res = {
        { "a", "b" },
        { "\n", " " },
        { "", "#" },
    };

    CSVStrategy strategy = CSVStrategy.DEFAULT_STRATEGY;
    assertEquals(CSVStrategy.COMMENTS_DISABLED, strategy.getCommentStart());

    CSVParser parser = new CSVParser(new StringReader(code), strategy);
    String[][] tmp = parser.getAllValues();
    assertTrue(tmp.length > 0);

    if (!CSVPrinterTest.equals(res, tmp)) {
      assertTrue(false);
    }

    String[][] res_comments = {
        { "a", "b" },
        { "\n", " " },
        { ""},
    };

    strategy = new CSVStrategy(',','"','#');
    parser = new CSVParser(new StringReader(code), strategy);
    tmp = parser.getAllValues();

    if (!CSVPrinterTest.equals(res_comments, tmp)) {
      assertTrue(false);
    }
  }


    public void testUnicodeEscape() throws IOException {
      String code = "abc,\\u0070\\u0075\\u0062\\u006C\\u0069\\u0063";
      CSVParser parser = new CSVParser(new StringReader(code));
      parser.getStrategy().setUnicodeEscapeInterpretation(true);
      String[] data = parser.getLine();
      assertEquals(2, data.length);
      assertEquals("abc", data[0]);
      assertEquals("public", data[1]);
    }
    
    public void testCarriageReturnLineFeedEndings() throws IOException {
     String code = "foo\r\nbaar,\r\nhello,world\r\n,kanu";
     CSVParser parser = new CSVParser(new StringReader(code));
     String[][] data = parser.getAllValues();
     assertEquals(4, data.length);
    }
    
    public void testIgnoreEmptyLines() throws IOException {
      String code = "\nfoo,baar\n\r\n,\n\n,world\r\n\n";
      //String code = "world\r\n\n";
      //String code = "foo;baar\r\n\r\nhello;\r\n\r\nworld;\r\n";
      CSVParser parser = new CSVParser(new StringReader(code));
      String[][] data = parser.getAllValues();
      assertEquals(3, data.length);
    }
    
    public void testLineTokenConsistency() throws IOException {
      String code = "\nfoo,baar\n\r\n,\n\n,world\r\n\n";
      CSVParser parser = new CSVParser(new StringReader(code));
      String[][] data = parser.getAllValues();
      parser = new CSVParser(new StringReader(code));
      CSVParser parser1 = new CSVParser(new StringReader(code));
      for (int i = 0; i < data.length; i++) {
        assertTrue(Arrays.equals(parser1.getLine(), data[i]));
        for (int j = 0; j < data[i].length; j++) {
          assertEquals(parser.nextValue(), data[i][j]);
        }
      }
    }

    // From SANDBOX-153
     public void testDelimiterIsWhitespace() throws IOException {
         String code = "one\ttwo\t\tfour \t five\t six";
         TestCSVParser parser = new TestCSVParser(new StringReader(code), CSVStrategy.TDF_STRATEGY);
         assertEquals(CSVParser.TT_TOKEN + ";one;", parser.testNextToken());
         assertEquals(CSVParser.TT_TOKEN + ";two;", parser.testNextToken());
         assertEquals(CSVParser.TT_TOKEN + ";;", parser.testNextToken());
         assertEquals(CSVParser.TT_TOKEN + ";four;", parser.testNextToken());
         assertEquals(CSVParser.TT_TOKEN + ";five;", parser.testNextToken());
         assertEquals(CSVParser.TT_EOF + ";six;", parser.testNextToken());
     }
}
