/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you under the Educational
 * Community 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://opensource.org/licenses/ecl2.txt
 *
 * 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.opencastproject.series.impl;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.opencastproject.db.DBTestEnv.getDbSessionFactory;
import static org.opencastproject.db.DBTestEnv.newEntityManagerFactory;
import static org.opencastproject.util.data.Collections.list;

import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
import org.opencastproject.message.broker.api.update.SeriesUpdateHandler;
import org.opencastproject.metadata.dublincore.DublinCore;
import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
import org.opencastproject.metadata.dublincore.DublinCoreCatalogService;
import org.opencastproject.metadata.dublincore.DublinCoreUtil;
import org.opencastproject.metadata.dublincore.DublinCoreValue;
import org.opencastproject.metadata.dublincore.DublinCores;
import org.opencastproject.security.api.AccessControlEntry;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.AccessControlUtil;
import org.opencastproject.security.api.DefaultOrganization;
import org.opencastproject.security.api.JaxbRole;
import org.opencastproject.security.api.JaxbUser;
import org.opencastproject.security.api.Permissions;
import org.opencastproject.security.api.SecurityConstants;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.User;
import org.opencastproject.series.impl.persistence.SeriesServiceDatabaseImpl;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.PathSupport;

import com.entwinemedia.fn.data.Opt;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.easymock.EasyMock;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;

import java.io.File;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

/**
 * Test for Series Service.
 *
 */
public class SeriesServiceImplTest {

  private SeriesServiceDatabaseImpl seriesDatabase;
  private DublinCoreCatalogService dcService;
  private String root;

  private SeriesServiceImpl seriesService;

  private DublinCoreCatalog testCatalog;
  private DublinCoreCatalog testCatalog2;

  private static final String ELEMENT_TYPE = "testelement";
  private static final byte[] ELEMENT_DATA_1 = "abcdefghijklmnopqrstuvwxyz".getBytes();
  private static final byte[] ELEMENT_DATA_2 = "0123456789".getBytes();

  /**
   * @throws java.lang.Exception
   */
  @Before
  public void setUp() throws Exception {
    long currentTime = System.currentTimeMillis();

    // Mock up a security service
    SecurityService securityService = EasyMock.createNiceMock(SecurityService.class);
    User user = new JaxbUser("admin", "test", new DefaultOrganization(), new JaxbRole(
            SecurityConstants.GLOBAL_ADMIN_ROLE, new DefaultOrganization()));
    EasyMock.expect(securityService.getOrganization()).andReturn(new DefaultOrganization()).anyTimes();
    EasyMock.expect(securityService.getUser()).andReturn(user).anyTimes();
    EasyMock.replay(securityService);

    seriesDatabase = new SeriesServiceDatabaseImpl();
    seriesDatabase.setEntityManagerFactory(newEntityManagerFactory(SeriesServiceDatabaseImpl.PERSISTENCE_UNIT));
    seriesDatabase.setDBSessionFactory(getDbSessionFactory());
    dcService = new DublinCoreCatalogService();
    seriesDatabase.setDublinCoreService(dcService);
    seriesDatabase.setSecurityService(securityService);
    seriesDatabase.activate(null);

    root = PathSupport.concat("target", Long.toString(currentTime));

    ElasticsearchIndex esIndex = EasyMock.createNiceMock(ElasticsearchIndex.class);

    EasyMock.expect(esIndex.addOrUpdateSeries(EasyMock.anyString(), EasyMock.anyObject(Function.class),
            EasyMock.anyString(), EasyMock.anyObject(User.class))).andReturn(Optional.empty()).atLeastOnce();
    EasyMock.replay(esIndex);

    seriesService = new SeriesServiceImpl();
    seriesService.setPersistence(seriesDatabase);
    seriesService.setSecurityService(securityService);
    seriesService.setElasticsearchIndex(esIndex);
    seriesService.addMessageHandler(EasyMock.createNiceMock(SeriesUpdateHandler.class));

    BundleContext bundleContext = EasyMock.createNiceMock(BundleContext.class);
    EasyMock.expect(bundleContext.getProperty((String) EasyMock.anyObject())).andReturn("System Admin");
    EasyMock.replay(bundleContext);

    ComponentContext componentContext = EasyMock.createNiceMock(ComponentContext.class);
    EasyMock.expect(componentContext.getBundleContext()).andReturn(bundleContext).anyTimes();
    EasyMock.replay(componentContext);

    seriesService.activate(componentContext);

    InputStream in = null;
    try {
      in = getClass().getResourceAsStream("/dublincore.xml");
      testCatalog = dcService.load(in);
    } finally {
      IOUtils.closeQuietly(in);
    }
    try {
      in = getClass().getResourceAsStream("/dublincore2.xml");
      testCatalog2 = dcService.load(in);
    } finally {
      IOUtils.closeQuietly(in);
    }
  }

  /**
   * @throws java.lang.Exception
   */
  @After
  public void tearDown() throws Exception {
    seriesDatabase = null;
    FileUtils.deleteQuietly(new File(root));
  }

  @Test
  public void testSeriesManagement() throws Exception {
    testCatalog.set(DublinCore.PROPERTY_TITLE, "Some title");
    seriesService.updateSeries(testCatalog);
    DublinCoreCatalog retrivedSeries = seriesService.getSeries(testCatalog.getFirst(DublinCore.PROPERTY_IDENTIFIER));
    Assert.assertEquals("Some title", retrivedSeries.getFirst(DublinCore.PROPERTY_TITLE));

    testCatalog.set(DublinCore.PROPERTY_TITLE, "Some other title");
    seriesService.updateSeries(testCatalog);
    retrivedSeries = seriesService.getSeries(testCatalog.getFirst(DublinCore.PROPERTY_IDENTIFIER));
    Assert.assertEquals("Some other title", retrivedSeries.getFirst(DublinCore.PROPERTY_TITLE));

    seriesService.deleteSeries(testCatalog.getFirst(DublinCore.PROPERTY_IDENTIFIER));
    try {
      seriesService.getSeries(testCatalog.getFirst(DublinCore.PROPERTY_IDENTIFIER));
      Assert.fail("Series should not be available after removal.");
    } catch (NotFoundException e) {
      // expected
    }
  }


  @Test
  public void testAddingSeriesWithoutID() throws Exception {
    testCatalog.remove(DublinCore.PROPERTY_IDENTIFIER);
    DublinCoreCatalog newSeries = seriesService.updateSeries(testCatalog);
    Assert.assertNotNull("New series DC should be returned", newSeries);
    String id = newSeries.getFirst(DublinCore.PROPERTY_IDENTIFIER);
    Assert.assertNotNull("New series should have id set", id);
  }

  @Test
  public void testACLManagement() throws Exception {
    // sample access control list
    AccessControlList accessControlList = new AccessControlList();
    List<AccessControlEntry> acl = accessControlList.getEntries();
    acl.add(new AccessControlEntry("admin", "delete", true));

    try {
      seriesService.updateAccessControl("failid", accessControlList);
      Assert.fail("Should fail when adding ACL to nonexistent series,");
    } catch (NotFoundException e) {
      // expected
    }

    seriesService.updateSeries(testCatalog);
    seriesService.updateAccessControl(testCatalog.getFirst(DublinCore.PROPERTY_IDENTIFIER), accessControlList);
    AccessControlList retrievedACL = seriesService.getSeriesAccessControl(testCatalog
            .getFirst(DublinCore.PROPERTY_IDENTIFIER));
    Assert.assertNotNull(retrievedACL);
    acl = retrievedACL.getEntries();
    Assert.assertEquals(acl.size(), 1);
    Assert.assertEquals("admin", acl.get(0).getRole());

    acl = accessControlList.getEntries();
    acl.clear();
    acl.add(new AccessControlEntry("student", Permissions.Action.READ.toString(), true));
    seriesService.updateAccessControl(testCatalog.getFirst(DublinCore.PROPERTY_IDENTIFIER), accessControlList);
    retrievedACL = seriesService.getSeriesAccessControl(testCatalog.getFirst(DublinCore.PROPERTY_IDENTIFIER));
    Assert.assertNotNull(retrievedACL);
    acl = retrievedACL.getEntries();
    Assert.assertEquals(acl.size(), 1);
    Assert.assertEquals("student", acl.get(0).getRole());
  }

  @Test
  public void testDublinCoreCatalogEquality1() {
    DublinCoreCatalog a = DublinCores.mkOpencast().getCatalog();
    DublinCoreCatalog b = DublinCores.mkOpencast().getCatalog();
    a.set(DublinCore.PROPERTY_IDENTIFIER, "123");
    assertFalse(DublinCoreUtil.equals(a, b));
    b.set(DublinCore.PROPERTY_IDENTIFIER, "123");
    assertTrue(DublinCoreUtil.equals(a, b));
    a.set(DublinCore.PROPERTY_CONTRIBUTOR, list(DublinCoreValue.mk("Peter"), DublinCoreValue.mk("Paul")));
    b.set(DublinCore.PROPERTY_CONTRIBUTOR, list(DublinCoreValue.mk("Paul"), DublinCoreValue.mk("Peter")));
    assertFalse(DublinCoreUtil.equals(a, b));
    //
    b.set(DublinCore.PROPERTY_CONTRIBUTOR, list(DublinCoreValue.mk("Peter"), DublinCoreValue.mk("Paul")));
    assertTrue(DublinCoreUtil.equals(a, b));
    //
    a.set(DublinCore.PROPERTY_SPATIAL, "room1");
    a.set(DublinCore.PROPERTY_DESCRIPTION, "this is a test lecture");
    b.set(DublinCore.PROPERTY_DESCRIPTION, "this is a test lecture");
    b.set(DublinCore.PROPERTY_SPATIAL, "room1");
    assertTrue(DublinCoreUtil.equals(a, b));
  }

  @Test
  public void testDublinCoreCatalogEquality2() {
    DublinCoreCatalog a = DublinCores.mkOpencast().getCatalog();
    DublinCoreCatalog b = DublinCores.mkOpencast().getCatalog();
    a.set(DublinCore.PROPERTY_DESCRIPTION, "this is a test lecture");
    a.set(DublinCore.PROPERTY_SPATIAL, "room1");
    a.set(DublinCore.PROPERTY_IDENTIFIER, "123");
    a.set(DublinCore.PROPERTY_CONTRIBUTOR, list(DublinCoreValue.mk("Peter"), DublinCoreValue.mk("Paul")));
    b.set(DublinCore.PROPERTY_CONTRIBUTOR, list(DublinCoreValue.mk("Peter"), DublinCoreValue.mk("Paul")));
    b.set(DublinCore.PROPERTY_DESCRIPTION, "this is a test lecture");
    b.set(DublinCore.PROPERTY_SPATIAL, "room1");
    b.set(DublinCore.PROPERTY_IDENTIFIER, "123");
    assertTrue(DublinCoreUtil.equals(a, b));
  }

  @Test
  public void testDublinCoreCatalogPreservation() throws Exception {
    seriesService.updateSeries(testCatalog2);
    DublinCoreCatalog dc = seriesService.getSeries("10.0000/5820");
    assertTrue(DublinCoreUtil.equals(testCatalog2, testCatalog2));
    assertTrue(DublinCoreUtil.equals(dc, dc));
    assertTrue(DublinCoreUtil.equals(testCatalog2, dc));
  }

  @Test
  public void testACLEquality1() {
    AccessControlList a = new AccessControlList(new AccessControlEntry("a", Permissions.Action.READ.toString(), true),
            new AccessControlEntry("b", Permissions.Action.WRITE.toString(), false));
    AccessControlList b = new AccessControlList(new AccessControlEntry("b", Permissions.Action.WRITE.toString(), false),
            new AccessControlEntry("a", Permissions.Action.READ.toString(), true));
    assertTrue(AccessControlUtil.equals(a, b));
  }

  @Test
  public void testACLEquality2() {
    AccessControlList a = new AccessControlList();
    AccessControlList b = new AccessControlList();
    assertTrue(AccessControlUtil.equals(a, b));
  }

  @Test
  public void testACLEquality3() {
    AccessControlList a = new AccessControlList();
    AccessControlList b = new AccessControlList(
            new AccessControlEntry("b", Permissions.Action.WRITE.toString(), false));
    assertFalse(AccessControlUtil.equals(a, b));
  }

  @Test
  public void testACLEquality4() {
    AccessControlList a = new AccessControlList(
            new AccessControlEntry("b", Permissions.Action.WRITE.toString(), false));
    AccessControlList b = new AccessControlList(new AccessControlEntry("b", Permissions.Action.WRITE.toString(), false),
            new AccessControlEntry("b", Permissions.Action.READ.toString(), false));
    assertFalse(AccessControlUtil.equals(a, b));
  }

  @Test
  public void testACLEquality5() {
    AccessControlList a = new AccessControlList(
            new AccessControlEntry("a", Permissions.Action.WRITE.toString(), false),
            new AccessControlEntry("a", Permissions.Action.WRITE.toString(), false));
    AccessControlList b = new AccessControlList(
            new AccessControlEntry("a", Permissions.Action.WRITE.toString(), false),
            new AccessControlEntry("b", Permissions.Action.READ.toString(), false));
    assertFalse(AccessControlUtil.equals(a, b));
  }

  @Test
  public void testACLEquality6() {
    AccessControlList a = new AccessControlList(
            new AccessControlEntry("a", Permissions.Action.WRITE.toString(), false),
            new AccessControlEntry("a", Permissions.Action.WRITE.toString(), false),
            new AccessControlEntry("b", Permissions.Action.READ.toString(), true));
    AccessControlList b = new AccessControlList(
            new AccessControlEntry("a", Permissions.Action.WRITE.toString(), false),
            new AccessControlEntry("b", Permissions.Action.READ.toString(), true));
    assertTrue(AccessControlUtil.equals(a, b));
  }

  @Test
  public void testACLEquality7() {
    // It is possible to apply this contradictory ACL to a series or event in OC,
    // where "a" is allowed AND disallowed from writing to the resource.
    // See MH-13089
    AccessControlList a = new AccessControlList(
            new AccessControlEntry("a", Permissions.Action.WRITE.toString(), false),
            new AccessControlEntry("a", Permissions.Action.WRITE.toString(), true),
            new AccessControlEntry("b", Permissions.Action.READ.toString(), true));
    AccessControlList b = new AccessControlList(
            new AccessControlEntry("a", Permissions.Action.WRITE.toString(), false),
            new AccessControlEntry("b", Permissions.Action.READ.toString(), true));
    assertFalse(AccessControlUtil.equals(a, b));
  }

  @Test
  public void testSeriesElements() throws Exception {
    seriesService.updateSeries(testCatalog);
    final String seriesId = testCatalog.getFirst(DublinCoreCatalog.PROPERTY_IDENTIFIER);

    assertTrue(seriesService.updateSeriesElement(seriesId, ELEMENT_TYPE, ELEMENT_DATA_1)); // creating
    assertTrue(seriesService.updateSeriesElement(seriesId, ELEMENT_TYPE, ELEMENT_DATA_1));  // updating
    assertArrayEquals(ELEMENT_DATA_1, seriesService.getSeriesElementData(seriesId, ELEMENT_TYPE).get());

    assertTrue(seriesService.updateSeriesElement(seriesId, ELEMENT_TYPE, ELEMENT_DATA_2));
    assertArrayEquals(ELEMENT_DATA_2, seriesService.getSeriesElementData(seriesId, ELEMENT_TYPE).get());

    assertTrue(seriesService.deleteSeriesElement(seriesId, ELEMENT_TYPE));
    assertFalse(seriesService.deleteSeriesElement(seriesId, ELEMENT_TYPE));
    assertEquals(Opt.none(), seriesService.getSeriesElementData(seriesId, ELEMENT_TYPE));
  }
}
