import junit.framework.TestCase;
import java.util.*;

/**
 * Test cases for the BookStore and Student classes.
 */
public class BookStoreTester extends TestCase {

  /* Book lists and price list for setting up test cases */
  private String bList1 = "book 1#book 2";
  private String bList2 = "book 3#book 4";
  private String bList3 = "book 3#book 5";
  private String bList4 = "book 5#book 6";
  private String pList = 
    "book 1:54.00:108.00#book 2:55.55:111.10#" + 
    "book 3:84.00:168.75#" +
    "book 4:34.50:70.75#book 5:40.00:80.00#" +
    "book 6:87.00:162.00";
  
  /* The BookReference and BookStore variables 
   * used for test cases in this class */
  private BookReference bref;
  private BookStore bstore;
  
  /**
   * Sorts a string made out of tokens. 
   * 
   * @param s The string to be sorted.
   * @param sep The separator between tokens.
   * 
   * @return A string made out of the same tokens sorted in
   * alphabetical order separated by the same separator.
   */
  private String sortTokenString(String s, String sep) {
    StringTokenizer st = new StringTokenizer(s, sep);
    ArrayList l = new ArrayList();
    while (st.hasMoreTokens()) 
      l.add(st.nextToken());
    String[] sarr = (String []) l.toArray(new String[1]);
    Arrays.sort(sarr);
    StringBuffer sb = new StringBuffer();
    if (sarr.length != 0)
      sb.append(sarr[0]);
    for (int i=1; i<sarr.length; i++)
      sb.append("#" + sarr[i]);
    return sb.toString();

  }
  
  private String sortInventory(String inv) {
    return sortTokenString(inv, "#");
  }
  
  private String sortNewlineSeparatedString(String output) {
    return sortTokenString(output, "\n");
  }
  
  private void compareTotals(String goodln, String userln) {
    // we do not compare totals
  }
  
  /**
   * Compares two lines in the output of inventoryBooks
   */
  private void compareLines(String goodln, String userln) {
    StringTokenizer goodst = new StringTokenizer(goodln, ",");
    StringTokenizer userst = new StringTokenizer(userln, ",");
    
    assertTrue("Line '" + userln + "' ended early", userst.hasMoreTokens());
    String userBookName = userst.nextToken();
    assertEquals("Book name '" +  userBookName +"' is wrong", 
                 goodst.nextToken(),
                 userBookName);
    assertTrue("Line '" + userln + "' ended early", userst.hasMoreTokens());
    int userNoBooks = Integer.parseInt(userst.nextToken().trim());
    int goodNoBooks = Integer.parseInt(goodst.nextToken().trim());
    assertEquals("Wrong no books '" + userNoBooks + "' for book '" + 
                 userBookName + "'", 
                 goodNoBooks,
                 userNoBooks);
    for (int i=0; i<4; i++) {
      assertTrue("Line '" + userln + "' ended early", userst.hasMoreTokens());
      double userValue = Double.parseDouble(userst.nextToken().trim());
      double goodValue = Double.parseDouble(goodst.nextToken().trim());
      assertEquals("Wrong value '" + userValue + "' instead of '" + goodValue +
                   "' at field " + (i+2) + " for book '" + userBookName + "'",
                   goodValue, userValue, 0.001);
    }
    assertFalse("Line '" + userln + "' is too long", userst.hasMoreTokens());
  }
  
  /**
   * Compares the inventoryBooks output to the known good output. This method is
   * needed because comparing the String representation of doubles is unreliable.
   */
  private void compareInventoryBooksOutput(String goodOutput, String userOutput) {
    goodOutput = sortNewlineSeparatedString(goodOutput);
    userOutput = sortNewlineSeparatedString(userOutput);
    
    StringTokenizer goodst = new StringTokenizer(goodOutput, "\n");
    StringTokenizer userst = new StringTokenizer(userOutput, "\n");
    
    while (goodst.hasMoreTokens()) {
      assertTrue("Output ended early", userst.hasMoreTokens());
      String goodln = goodst.nextToken();
      String userln = userst.nextToken();
      if (goodln.substring(0,6).equals("Total:")) 
        compareTotals(goodln, userln);
      else
        compareLines(goodln, userln);
    }
    
    assertFalse("Output is too long", userst.hasMoreTokens());
  }
  
  /**
   * This method is called before each test below
   */
  protected void setUp() {
    bref = new BookReference(bList1, bList2, bList3, bList4, pList);
  }
  
  /**
   * Tests the first (the simple) constructor.
   */
  public void testFirstConstructor() {
    bstore = new BookStore(bref, 0.8, 0.3);
    
    assertEquals("Costs should be 0", 0.0, bstore.getCosts(), 0.001);
    assertEquals("Sales should be 0", 0.0, bstore.getSales(), 0.001);
    assertEquals("Inventory should be empty", "", bstore.getInventory());
  }
  
  /**
   * Tests the addCosts method.
   */
  public void testAddCosts() {
    bstore = new BookStore(bref, 0.8, 0.3);
    
    assertEquals("Book 4 cost is wrong",
                 0.8 * 34.50, bstore.addCosts("book 4"), 0.001);
    assertEquals("Total costs are wrong (1)",
                 0.8 * 34.50, bstore.getCosts(), 0.001);
    assertEquals("Book 1 cost is wrong",
                 0.8 * 54.00, bstore.addCosts("book 1"), 0.001);
    assertEquals("Total costs are wrong (2)",
                 0.8 * (54.00+34.50), bstore.getCosts(), 0.001);
  }
  
  /**
   * Tests the addSales method (without trying to pass it books that
   * are not on the price list)
   */
  public void testAddSalesOnlyFromPriceList() {
    bstore = new BookStore(bref, 0.8, 0.3);
    
    assertEquals("Book 2 sale value is wrong",
                 0.3 * 111.10, bstore.addSales("book 2"), 0.001);
    assertEquals("Total sales are wrong (1)",
                 0.3 * 111.10, bstore.getSales(), 0.001);
    assertEquals("Book 3 sale value is wrong",
                 0.3 * 168.75, bstore.addSales("book 3"), 0.001);
    assertEquals("Total sales are wrong (2)",
                 0.3 * (111.10+168.75), bstore.getSales(), 0.001);  
  }
  
  /**
   * Tests the addSales method by also passing a book that is not in the
   * book list.
   */
  public void testAddSalesNotInPriceList() {
     bstore = new BookStore(bref, 0.8, 0.3);
    
     assertEquals("Book 2 sale value is wrong",
                  0.3 * 111.10, bstore.addSales("book 2"), 0.001);
     assertEquals("Total sales are wrong (1)",
                  0.3 * 111.10, bstore.getSales(), 0.001);
     assertEquals("Adding book not in price list should return 0",
                  0.0, bstore.addSales("invalid book"), 0.001);
     assertEquals("Total sales are wrong (2)",
                   0.3 * 111.10, bstore.getSales(), 0.001);     
  }
  
  /**
   * Tests the bookAvalaible method.
   */
  public void testBookAvailable() {
    bstore = new BookStore(bref, 0.8, 0.3);    
    
    bstore.addInventory("book 1");
    bstore.addInventory("book 2");
    
    assertTrue("'book 1' is in the inventory", 
               bstore.bookAvailable("book 1"));
    assertTrue("'book 2' is in the inventory", 
               bstore.bookAvailable("book 2"));
    assertFalse("'book 3' is not in the inventory", 
               bstore.bookAvailable("book 3"));
  }
  
  /**
   * Tests the buyBook method.
   */
  public void testBuyBook() {
    bstore = new BookStore(bref, 0.8, 0.3, 
                           "book 1#book 1#book 2#book 5#book 2#book 1#book 5");
    
    double prevCosts = bstore.getCosts();
    assertEquals("Cost for book 1 is wrong",
                 0.8 * 54.00, bstore.buyBook("book 1"), 0.001);
    double currCosts = bstore.getCosts();
    assertEquals("Total costs were not updated properly", 
                 0.8 * 54.00, currCosts - prevCosts, 0.001);
    assertEquals("Inventory is wrong", 
                 sortInventory("book 1:4#book 5:2#book 2:2"),
                 sortInventory(bstore.getInventory()));
    assertEquals("Book not on the book lists",
                 0.0, bstore.buyBook("book X"), 0.001);
  }
  
  /**
   * Tests the sellBook method.
   */
  public void testSellBook() {   
    bstore = new BookStore(bref, 0.8, 0.3, 
                           "book 1#book 1#book 3#book 5#book 3#book 1#book 5");
    
    double prevSales = bstore.getSales();
    assertEquals("Sale price for book 3 is wrong",
                 0.3 * 168.75, bstore.sellBook("book 3"), 0.001);
    double currSales = bstore.getSales();
    assertEquals("Total sales were not updated properly", 
                 currSales - prevSales, 0.3*168.75, 0.001);
    assertEquals("Inventory is wrong", 
                 sortInventory("book 1:3#book 5:2#book 3:1"),
                 sortInventory(bstore.getInventory()));
    assertEquals("Book not in inventory, should return 0",
                 0.0, bstore.sellBook("book 4"), 0.001);
  }
  
  /**
   * Tests the reduceInventory method
   */
  public void testReduceInventory() {
      bstore = new BookStore(bref, 0.8, 0.3, 
                           "book 1#book 1#book 3#book 5#book 5");  
 
      bstore.reduceInventory("book 1");
      assertEquals("Inventory is wrong (1)", 
                 sortInventory("book 1:1#book 3:1#book 5:2"),
                 sortInventory(bstore.getInventory()));   
      bstore.reduceInventory("book 3");
      assertEquals("Inventory is wrong (2)", 
                 sortInventory("book 1:1#book 5:2"),
                 sortInventory(bstore.getInventory()));   
      bstore.reduceInventory("book 1");
      bstore.reduceInventory("book 5");
      assertEquals("Inventory is wrong (3)", 
                   sortInventory("#book 5:1"),
                   sortInventory(bstore.getInventory()));   
      bstore.reduceInventory("book 5");
      assertEquals("Inventory is wrong (4)", 
                   "",
                   bstore.getInventory());   
  }
  
  /**
   * Tests the second constructor (that it performs all the actions
   * in the specification)
   */
  public void testConstructor2() {
    bstore = new BookStore(bref, 0.8, 0.3, 
                           "book 1#book 1#book 3#book 5#book 6#book 5#book 6#book 6#book 6");
    assertEquals("Inventory is wrong",
                 sortInventory("book 1:2#book 3:1#book 5:2#book 6:4"),
                 sortInventory(bstore.getInventory()));
    assertEquals("Total costs are wrong",
                 0.8 * (2 * 54.00 + 1 * 84.00 + 2 * 40.00 + 4 * 87.00),
                 bstore.getCosts(),
                 0.001);
  }
  
  /**
   * Tests the inventoryBooks method
   */
  public void testInventoryBooks() {
    bstore = new BookStore(bref, 0.8, 0.3, 
                           "book 1#book 1#book 3#book 5#book 6#book 5#book 6#book 6#book 6");
    String goodOutput = "book 1, 2, 43.2, 86.4, 32.4, 64.8\n" +
      "book 3, 1, 67.2, 67.2, 50.625, 50.625\n" +
      "book 5, 2, 32.0, 64.0, 24.0, 48.0\n" +
      "book 6, 4, 69.6, 278.4, 48.6, 194.4\n"+
      "Total: number of books: 8, cost: 496.0, sale price: 357.825";

    compareInventoryBooksOutput(goodOutput, bstore.inventoryBooks());
  }
  
  /**
   * Tests that the inventory string is properly formed 
   * (no leading or trailing '#')
   */
  public void testInventoryStructure() {
    bstore = new BookStore(bref, 0.8, 0.3, 
                           "book 1#book 1#book 3#book 5#book 6#book 5#book 6#book 6#book 6");
    bstore.addInventory("book 1");
    bstore.addInventory("book 6");
    String inv = bstore.getInventory();
    assertTrue("Inventory string should not be empty",
               inv.length() > 0);
    assertTrue("First char in inventory should not be #: " + inv,
                inv.charAt(0) != '#');
    assertTrue("Last char in inventory should not be #: " + inv,
                inv.charAt(inv.length()-1) != '#');
    
  }
  
  /************ Tests for the Student class ********************************/
  

  
  /**
   * Tests the buyBooks method without any initial books in the 
   * student's posession. Tests the state of all classes involved.
   */
  public void testStudentBuyBooksNoInitialBooksFull() {
    BookReference bref = new BookReference(bList1, bList2, bList3, bList4, pList);
    BookStore bstore = new BookStore(bref, 0.8, 0.3, 
                           "book 1#book 1#book 3#book 5#book 6#book 5#book 6#book 6#book 6");
    University univ = new University(bref, bstore);
    Student s = new Student("Johnny", "123", 3, univ);
    
    s.buyBooks();
    assertEquals("Book list is wrong", 
                 sortNewlineSeparatedString("book 3\nbook 5\n"),
                 sortNewlineSeparatedString(s.getBooks()));
    assertEquals("Total book cost is wrong",
                 74.625,
                 s.getBookCosts(),
                 0.001);
    assertEquals("Inventory of book store was not updated properly",
                 sortInventory("book 1:2#book 5:1#book 6:4"),
                 sortInventory(bstore.getInventory()));
  }
  
  /**
   * Tests the sellBooks method. To test this, buyBooks is first called so
   * the student gets some books. Also, changeYear is involved.
   */
 public void testStudentSellBooks() {
    BookReference bref = new BookReference(bList1, bList2, bList3, bList4, pList);
    BookStore bstore = new BookStore(bref, 0.8, 0.3, 
                           "book 1#book 1#book 3#book 5#book 6#book 5#book 6#book 6#book 6");
    University univ = new University(bref, bstore);
    Student s = new Student("Johnny", "123", 3, univ);
    
    s.buyBooks();
    assertEquals("Book list is wrong", 
                 sortNewlineSeparatedString("book 3\nbook 5\n"),
                 sortNewlineSeparatedString(s.getBooks()));
    assertEquals("Total book cost is wrong",
                 74.625,
                 s.getBookCosts(),
                 0.001);
    
    s.changeYear();
    s.sellBooks();
    
    assertEquals("Book list after sale is wrong",
                 sortNewlineSeparatedString("book 5\n"),
                 sortNewlineSeparatedString(s.getBooks()));     
    assertEquals("Total book cost is wrong",
                 7.425,
                 s.getBookCosts(),
                 0.001);
  }

 /** 
  * Tests the buyBooks method with some books in the student's possession.
  * (so buyBooks is called twice, first time to get some books, the second
  * time to verify that it correctly performs the required steps: selling
  * the books that are no longer need, etc.)
  */
 public void testStudentBuyBooksWithBooks() {
    BookReference bref = new BookReference(bList1, bList2, bList3, bList4, pList);
    BookStore bstore = new BookStore(bref, 0.8, 0.3, 
                           "book 1#book 1#book 3#book 5#book 6#book 5#book 6#book 6#book 6");
    University univ = new University(bref, bstore);
    Student s = new Student("Johnny", "123", 3, univ);
    
    s.buyBooks();
    assertEquals("Book list is wrong", 
                 sortNewlineSeparatedString("book 3\nbook 5\n"),
                 sortNewlineSeparatedString(s.getBooks()));
    assertEquals("Total book cost is wrong",
                 74.625,
                 s.getBookCosts(),
                 0.001);
    
    s.changeYear();
    s.buyBooks();
    assertEquals("Book list is wrong", 
                 sortNewlineSeparatedString("book 6\nbook 5\n"),
                 sortNewlineSeparatedString(s.getBooks()));
    assertEquals("Total book cost is wrong",
                 7.425 + 48.6,
                 s.getBookCosts(),
                 0.001);    
  }
 
 /**
  * Tests a change of schools along with a situation in which the student
  * cannot sell all the books back, because the book store won't accept them
  */
 public void testStudentChangeSchools() {
   BookReference bref = new BookReference(bList1, bList2, 
                                          "book 3#book 5#book 6", 
                                          "book 1", pList);
   BookStore bstore = new BookStore(bref, 0.8, 0.3, 
                           "book 1#book 1#book 3#book 5#book 6#book 5#book 6#book 6#book 6");
   University univ = new University(bref, bstore);
   Student s = new Student("Johnny", "123", 3, univ);
   
   s.buyBooks();
   /* now student should have book 3, book 5 and book 6 */
   
   BookReference newbref = new BookReference("book 1", "book 5", "book 1", "book 1", 
                                             "book 1:10.00:20.00#book 5:10.00:20.00");
   BookStore newbstore = new BookStore(newbref, 0.8, 0.3, "book 1#book 1");
   University newuniv = new University(newbref, newbstore);
   
   s.changeSchools(newuniv);
   s.changeYear();
   
   /* he tries to sell all books back, but the book store accepts only book 5 */
   s.sellBooks();
   assertEquals("Book list is wrong", 
                sortNewlineSeparatedString("book 3\nbook 6\n"),
                sortNewlineSeparatedString(s.getBooks()));
 }
 
 /**
  * Tests that whenever the student cannot buy all the books he needs,
  * the list of books still needed is properly returned.
  */
 public void testStudentBooksStillNeeded() {
    BookReference bref = new BookReference(bList1, bList2, 
                                           "book 1#book 2#book 3#book 4#book 5#book 6", 
                                           bList4, pList);
    BookStore bstore = new BookStore(bref, 0.8, 0.3, 
                           "book 3#book 5#book 6#book 5#book 6#book 6#book 6");
    University univ = new University(bref, bstore);
    Student s = new Student("Johnny", "123", 3, univ);
    
    /* he tries to buy all six books, but only three of them are available */
    assertEquals("Book still needed list is wrong",
                 sortInventory("book 2#book 1#book 4"),
                 sortInventory(s.buyBooks()));
 }
 
 /**
  * Tests that the string returned by getBooks is properly formed.
  */
 public void testStudentGetBooksStructure() {
    BookReference bref = new BookReference(bList1, bList2, 
                                          "book 3#book 5#book 6", 
                                          "book 1", pList);
    BookStore bstore = new BookStore(bref, 0.8, 0.3, 
                           "book 1#book 1#book 3#book 5#book 6#book 5#book 6#book 6#book 6");
    University univ = new University(bref, bstore);
    Student s = new Student("Johnny", "123", 3, univ);
   
    s.buyBooks();
    /* now student should have book 3, book 5 and book 6 */
   
    String getb = s.getBooks();
    
    assertTrue("getBooks should not be empty",
                getb.length() > 0);
    assertTrue("First char of getBooks should not be '\\n'",
               getb.charAt(0) != '\n');
    assertTrue("Last char of getBooks should be '\\n'",
               getb.charAt(getb.length()-1) == '\n');
    assertTrue("No consecutive '\\n' in getBooks",
               getb.indexOf("\n\n") == -1);
 }
 
}

