/*
 * Copyright (c) 2016 Farooq Khan
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package io.jsondb.tests;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;

import java.io.File;
import java.security.GeneralSecurityException;
import java.util.Comparator;
import java.util.List;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import com.google.common.io.Files;

import io.jsondb.InvalidJsonDbApiUsageException;
import io.jsondb.JsonDBTemplate;
import io.jsondb.Util;
import io.jsondb.crypto.DefaultAESCBCCipher;
import io.jsondb.crypto.ICipher;
import io.jsondb.tests.model.Instance;

/**
 * Unit tests that cover all aspects of find queries.
 *
 * @author Farooq Khan
 * @version 1.0 31 Dec 2015
 */
public class FindQueryTests {

  private String dbFilesLocation = "src/test/resources/dbfiles/findQueryTests";
  private File dbFilesFolder = new File(dbFilesLocation);
  private File instancesJson = new File(dbFilesFolder, "instances.json");

  private JsonDBTemplate jsonDBTemplate = null;

  @Rule
  public ExpectedException expectedException = ExpectedException.none();

  @Before
  public void setUp() throws Exception {
    dbFilesFolder.mkdir();
    Files.copy(new File("src/test/resources/dbfiles/instances.json"), instancesJson);
    ICipher cipher = null;
    try {
      cipher = new DefaultAESCBCCipher("1r8+24pibarAWgS85/Heeg==");
    } catch (GeneralSecurityException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }

    jsonDBTemplate = new JsonDBTemplate(dbFilesLocation, "io.jsondb.tests.model", cipher);
  }

  @After
  public void tearDown() throws Exception {
    Util.delete(dbFilesFolder);
  }

  /**
   * test to find all documents for a collection type
   */
  @Test
  public void testFind_AllDocumentsForType() {
    String jxQuery = "."; //XPATH for all elements in a collection
    List<Instance> instances = jsonDBTemplate.find(jxQuery, Instance.class);
    assertEquals(instances.size(), 6);
  }

  /**
   * test to find a existing document
   */
  @Test
  public void testFind_DocumentThatExists() {
    String jxQuery = String.format("/.[id='%s']", "01");
    List<Instance> instances = jsonDBTemplate.find(jxQuery, Instance.class);
    assertEquals(instances.size(), 1);
  }

  /**
   * test to find a non-existent document
   */
  @Test
  public void testFind_DocumentThatDoesNotExist() {
    String jxQuery = String.format("/.[id='%s']", "00");
    List<Instance> instances = jsonDBTemplate.find(jxQuery, Instance.class);
    assertNotNull(instances);
    assertEquals(instances.size(), 0);
  }

  /**
   * test to find all documents for a valid collection type
   */
  @Test
  public void testFindAll_ForValidCollectionType() {
    List<Instance> instances = jsonDBTemplate.findAll(Instance.class);
    assertEquals(instances.size(), 6);
  }

  private class NonAnotatedClass {}

  /**
   * test to find all documents for a Entity type which does not have @Document annotation
   */
  @Test
  public void testFindAll_ForInvalidCollectionType() {
    expectedException.expect(InvalidJsonDbApiUsageException.class);
    expectedException.expectMessage("Entity 'NonAnotatedClass' is not annotated with annotation @Document");
    jsonDBTemplate.findAll(NonAnotatedClass.class);
  }

  /**
   * test to find all documents for a unknown collection name
   */
  @Test
  public void testFindAll_UnknownCollection() {
    expectedException.expect(InvalidJsonDbApiUsageException.class);
    expectedException.expectMessage("Collection by name 'SomeCollection' not found. Create collection first");
    jsonDBTemplate.findAll("SomeCollection");
  }

  /**
   * test to find a single document from the complete collection.
   */
  @Test
  public void testFindOne_Document() {
    String jxQuery = "."; //XPATH for all elements in a collection
    Instance instance = jsonDBTemplate.findOne(jxQuery, Instance.class);
    assertNotNull(instance);
  }

  /**
   * test to find a single document for a query that returns only one document.
   */
  @Test
  public void testFindOne_SingleDocumentFromOne() {
    String jxQuery = String.format("/.[id='%s']", "01");
    Instance instance = jsonDBTemplate.findOne(jxQuery, Instance.class);
    assertNotNull(instance);
    assertEquals(instance.getId(), "01");
  }

  /**
   * test to find a single document for a query that can returns more than one document.
   */
  @Test
  public void testFindOne_SingleDocumentFromMany() {
    String jxQuery = String.format("/.[publicKey='%s']", "d3aa045f71bf4d1dffd2c5f485a4bc1d");
    Instance instance = jsonDBTemplate.findOne(jxQuery, Instance.class);
    assertNotNull(instance);
  }

  /**
   * test to find a document for Entity type which does not have @Document annotation
   */
  @Test
  public void testFindOne_NonAnotatedClass() {
    expectedException.expect(InvalidJsonDbApiUsageException.class);
    expectedException.expectMessage("Entity 'NonAnotatedClass' is not annotated with annotation @Document");
    jsonDBTemplate.findOne("000000", NonAnotatedClass.class);
  }

  /**
   * test to find a document for a unknown collection name
   */
  @Test
  public void testFindOne_UnknownCollection() {
    expectedException.expect(InvalidJsonDbApiUsageException.class);
    expectedException.expectMessage("Collection by name 'SomeCollection' not found. Create collection first");
    jsonDBTemplate.findOne("000000", "SomeCollection");
  }

  /**
   * test to find a single document with a non-existent id.
   */
  @Test
  public void testFindOne_NonExistentId() {
    String jxQuery = String.format("/.[id='%s']", "000000");
    Instance instance = jsonDBTemplate.findOne(jxQuery, Instance.class);
    assertNull(instance);
  }

  /**
   * a test that demonstrates how to query for a attribute with null value
   */
  @Test
  public void testFindQuery_DocumentWithNullAttribute() {
    String jxQuery = "/.[publicKey='']";
    Instance c = jsonDBTemplate.findOne(jxQuery, Instance.class);
    assertNotNull(c);
    assertEquals(c.getId(), "06");
  }

  /**
   * a test that demonstrates how to query for a attribute that is not null.
   */
  @Test
  public void testFindQuery_ForDocumentWithAttributeNotNull() {
    String jxQuery = "/.[not(publicKey='')]";
    List<Instance> instances = jsonDBTemplate.find(jxQuery, Instance.class);
    assertNotNull(instances);
    assertEquals(instances.size(), 5);
    for (Instance c : instances) {
      assertNotEquals(c.getId(), "06");
    }
  }

  /**
   * a test that demonstrates how to find while using sorting
   */
  @Test
  public void testFindQuery_AndSort() {
    String jxQuery = "."; //XPATH for all elements in a collection
    Comparator<Instance> comparator = new Comparator<Instance>() {
      @Override
      public int compare(Instance o1, Instance o2) {
        return (o1.getHostname().compareTo(o2.getHostname()));
      }
    };
    List<Instance> instances = jsonDBTemplate.find(jxQuery, Instance.class, comparator);
    assertEquals(instances.size(), 6);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-01");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-02");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-03");
    assertEquals(instances.get(3).getHostname(), "ec2-54-191-04");
    assertEquals(instances.get(4).getHostname(), "ec2-54-191-05");
    assertEquals(instances.get(5).getHostname(), "ec2-54-191-06");

    instances = jsonDBTemplate.find(jxQuery, Instance.class, comparator.reversed());
    assertEquals(instances.size(), 6);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-06");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-05");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-04");
    assertEquals(instances.get(3).getHostname(), "ec2-54-191-03");
    assertEquals(instances.get(4).getHostname(), "ec2-54-191-02");
    assertEquals(instances.get(5).getHostname(), "ec2-54-191-01");
  }

  /**
   * a test that demonstrates how to findAll while using sorting
   */
  @Test
  public void testFindAllQuery_AndSort() {
    Comparator<Instance> comparator = new Comparator<Instance>() {
      @Override
      public int compare(Instance o1, Instance o2) {
        return (o1.getHostname().compareTo(o2.getHostname()));
      }
    };
    List<Instance> instances = jsonDBTemplate.findAll(Instance.class, comparator);
    assertEquals(instances.size(), 6);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-01");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-02");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-03");
    assertEquals(instances.get(3).getHostname(), "ec2-54-191-04");
    assertEquals(instances.get(4).getHostname(), "ec2-54-191-05");
    assertEquals(instances.get(5).getHostname(), "ec2-54-191-06");

    instances = jsonDBTemplate.findAll(Instance.class, comparator.reversed());
    assertEquals(instances.size(), 6);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-06");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-05");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-04");
    assertEquals(instances.get(3).getHostname(), "ec2-54-191-03");
    assertEquals(instances.get(4).getHostname(), "ec2-54-191-02");
    assertEquals(instances.get(5).getHostname(), "ec2-54-191-01");
  }

  /**
   * a test that demonstrates how to find while using sorting and slicing
   */
  @Test
  public void testFindQuery_AndSortAndSlice() {
    String jxQuery = "."; //XPATH for all elements in a collection
    Comparator<Instance> comparator = new Comparator<Instance>() {
      @Override
      public int compare(Instance o1, Instance o2) {
        return (o1.getHostname().compareTo(o2.getHostname()));
      }
    };
    List<Instance> instances = jsonDBTemplate.find(jxQuery, Instance.class, comparator, ":5:2");
    assertEquals(instances.size(), 3);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-01");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-03");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-05");

    List<Instance> instances2 = jsonDBTemplate.find(jxQuery, Instance.class, comparator, ":5:2");
    assertEquals(instances2.size(), 3);
    assertEquals(instances2.get(0).getHostname(), "ec2-54-191-01");
    assertEquals(instances2.get(1).getHostname(), "ec2-54-191-03");
    assertEquals(instances2.get(2).getHostname(), "ec2-54-191-05");
    //Check deep copy is indeed working
    assertNotEquals(instances.get(0), instances2.get(0));
    assertNotEquals(instances.get(1), instances2.get(1));
    assertNotEquals(instances.get(2), instances2.get(2));

    instances = jsonDBTemplate.find(jxQuery, Instance.class, comparator, "::-2");
    assertEquals(instances.size(), 3);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-06");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-04");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-02");

    //This is double reverse test.
    instances = jsonDBTemplate.find(jxQuery, Instance.class, comparator.reversed(), "::-2");
    assertEquals(instances.size(), 3);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-01");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-03");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-05");
  }

  /**
   * a test that demonstrates how to find while using slicing without sorting
   */
  @Test
  public void testFindQuery_AndSlice() {
    String jxQuery = "."; //XPATH for all elements in a collection

    List<Instance> instances = jsonDBTemplate.find(jxQuery, Instance.class, null, ":5:2");
    assertEquals(instances.size(), 3);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-01");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-04");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-02");

    instances = jsonDBTemplate.find(jxQuery, Instance.class, null, "::-2");
    assertEquals(instances.size(), 3);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-06");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-03");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-05");
  }

  /**
   * a test that demonstrates how to find while using sorting and slicing
   */
  @Test
  public void testFindAllQuery_AndSortAndSlice() {
    Comparator<Instance> comparator = new Comparator<Instance>() {
      @Override
      public int compare(Instance o1, Instance o2) {
        return (o1.getHostname().compareTo(o2.getHostname()));
      }
    };
    List<Instance> instances = jsonDBTemplate.findAll(Instance.class, comparator, ":5:2");
    assertEquals(instances.size(), 3);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-01");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-03");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-05");

    List<Instance> instances2 = jsonDBTemplate.findAll(Instance.class, comparator, ":5:2");
    assertEquals(instances2.size(), 3);
    assertEquals(instances2.get(0).getHostname(), "ec2-54-191-01");
    assertEquals(instances2.get(1).getHostname(), "ec2-54-191-03");
    assertEquals(instances2.get(2).getHostname(), "ec2-54-191-05");
    //Check deep copy is indeed working
    assertNotEquals(instances.get(0), instances2.get(0));
    assertNotEquals(instances.get(1), instances2.get(1));
    assertNotEquals(instances.get(2), instances2.get(2));

    instances = jsonDBTemplate.findAll(Instance.class, comparator, "::-2");
    assertEquals(instances.size(), 3);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-06");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-04");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-02");

    //This is double reverse test.
    instances = jsonDBTemplate.findAll(Instance.class, comparator.reversed(), "::-2");
    assertEquals(instances.size(), 3);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-01");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-03");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-05");
  }

  /**
   * a test that demonstrates how to find while using slicing without sorting
   */
  @Test
  public void testFindAllQuery_AndSlice() {
    List<Instance> instances = jsonDBTemplate.findAll(Instance.class, null, ":5:2");
    assertEquals(instances.size(), 3);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-01");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-04");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-02");

    instances = jsonDBTemplate.findAll(Instance.class, null, "::-2");
    assertEquals(instances.size(), 3);
    assertEquals(instances.get(0).getHostname(), "ec2-54-191-06");
    assertEquals(instances.get(1).getHostname(), "ec2-54-191-03");
    assertEquals(instances.get(2).getHostname(), "ec2-54-191-05");
  }
}