/**
 * 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.mahout.cf.taste.impl.model.file;

import org.apache.commons.lang.mutable.MutableBoolean;
import org.apache.mahout.cf.taste.common.TasteException;
import org.apache.mahout.cf.taste.impl.TasteTestCase;
import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator;
import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;
import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;
import org.apache.mahout.cf.taste.impl.similarity.PearsonCorrelationSimilarity;
import org.apache.mahout.cf.taste.model.DataModel;
import org.apache.mahout.cf.taste.model.Preference;
import org.apache.mahout.cf.taste.model.PreferenceArray;
import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood;
import org.apache.mahout.cf.taste.recommender.Recommender;
import org.apache.mahout.cf.taste.similarity.UserSimilarity;
import org.junit.Before;
import org.junit.Test;

import java.io.File;
import java.util.NoSuchElementException;

/** <p>Tests {@link FileDataModel}.</p> */
public final class FileDataModelTest extends TasteTestCase {

  private static final String[] DATA = {
      "123,456,0.1",
      "123,789,0.6",
      "123,654,0.7",
      "234,123,0.5",
      "234,234,1.0",
      "234,999,0.9",
      "345,789,0.6",
      "345,654,0.7",
      "345,123,1.0",
      "345,234,0.5",
      "345,999,0.5",
      "456,456,0.1",
      "456,789,0.5",
      "456,654,0.0",
      "456,999,0.2",};

  private DataModel model;
  private File testFile;

  @Override
  @Before
  public void setUp() throws Exception {
    super.setUp();
    testFile = getTestTempFile("test.txt");
    writeLines(testFile, DATA);
    model = new FileDataModel(testFile);
  }

  @Test
  public void testFile() throws Exception {
    UserSimilarity userSimilarity = new PearsonCorrelationSimilarity(model);
    UserNeighborhood neighborhood = new NearestNUserNeighborhood(3, userSimilarity, model);
    Recommender recommender = new GenericUserBasedRecommender(model, neighborhood, userSimilarity);
    assertEquals(1, recommender.recommend(123, 3).size());
    assertEquals(0, recommender.recommend(234, 3).size());
    assertEquals(1, recommender.recommend(345, 3).size());

    // Make sure this doesn't throw an exception
    model.refresh(null);
  }

  @Test
  public void testTranspose() throws Exception {
    FileDataModel tModel = new FileDataModel(testFile, true, FileDataModel.DEFAULT_MIN_RELOAD_INTERVAL_MS);
    PreferenceArray userPrefs = tModel.getPreferencesFromUser(456);
    assertNotNull("user prefs are null and it shouldn't be", userPrefs);
    PreferenceArray pref = tModel.getPreferencesForItem(123);
    assertNotNull("pref is null and it shouldn't be", pref);
    assertEquals("pref Size: " + pref.length() + " is not: " + 3, 3, pref.length());
  }

  @Test  
  public void testGetItems() throws Exception {
    LongPrimitiveIterator it = model.getItemIDs();
    assertNotNull(it);
    assertTrue(it.hasNext());
    assertEquals(123, it.nextLong());
    assertTrue(it.hasNext());
    assertEquals(234, it.nextLong());
    assertTrue(it.hasNext());
    assertEquals(456, it.nextLong());
    assertTrue(it.hasNext());
    assertEquals(654, it.nextLong());
    assertTrue(it.hasNext());
    assertEquals(789, it.nextLong());
    assertTrue(it.hasNext());
    assertEquals(999, it.nextLong());
    assertFalse(it.hasNext());
    try {
      it.next();
      fail("Should throw NoSuchElementException");
    } catch (NoSuchElementException nsee) {
      // good
    }
  }

  @Test
  public void testPreferencesForItem() throws Exception {
    PreferenceArray prefs = model.getPreferencesForItem(456);
    assertNotNull(prefs);
    Preference pref1 = prefs.get(0);
    assertEquals(123, pref1.getUserID());
    assertEquals(456, pref1.getItemID());
    Preference pref2 = prefs.get(1);
    assertEquals(456, pref2.getUserID());
    assertEquals(456, pref2.getItemID());
    assertEquals(2, prefs.length());
  }

  @Test
  public void testGetNumUsers() throws Exception {
    assertEquals(4, model.getNumUsers());
  }

  @Test
  public void testNumUsersPreferring() throws Exception {
    assertEquals(2, model.getNumUsersWithPreferenceFor(456));
    assertEquals(0, model.getNumUsersWithPreferenceFor(111));
    assertEquals(0, model.getNumUsersWithPreferenceFor(111, 456));
    assertEquals(2, model.getNumUsersWithPreferenceFor(123, 234));
  }

  @Test
  public void testRefresh() throws Exception {
    final MutableBoolean initialized = new MutableBoolean(false);
    Runnable initializer = new Runnable() {
      @Override
      public void run() {
        try {
          model.getNumUsers();
          initialized.setValue(true);
        } catch (TasteException te) {
          // oops
        }
      }
    };
    new Thread(initializer).start();
    Thread.sleep(1000L); // wait a second for thread to start and call getNumUsers()
    model.getNumUsers(); // should block
    assertTrue(initialized.booleanValue());
    assertEquals(4, model.getNumUsers());
  }

  @Test
  public void testExplicitRefreshAfterCompleteFileUpdate() throws Exception {
    File file = getTestTempFile("refresh");
    writeLines(file, "123,456,3.0");

    /* create a FileDataModel that always reloads when the underlying file has changed */
    FileDataModel dataModel = new FileDataModel(file, false, 0L);
    assertEquals(3.0f, dataModel.getPreferenceValue(123L, 456L), EPSILON);

    /* change the underlying file,
     * we have to wait at least a second to see the change in the file's lastModified timestamp */
    Thread.sleep(2000L);
    writeLines(file, "123,456,5.0");
    dataModel.refresh(null);

    assertEquals(5.0f, dataModel.getPreferenceValue(123L, 456L), EPSILON);
  }

  @Test
  public void testToString() {
    assertTrue(model.toString().length() > 0);
  }

}
