/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2013-2015 ForgeRock AS.
 */
package org.opends.server.replication.plugin;

import static org.assertj.core.api.Assertions.*;
import static org.forgerock.opendj.ldap.ModificationType.*;
import static org.mockito.Mockito.*;
import static org.testng.Assert.*;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ModificationType;
import org.opends.server.TestCaseUtils;
import org.opends.server.core.DirectoryServer;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.common.CSN;
import org.opends.server.replication.common.CSNGenerator;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.Attributes;
import org.opends.server.types.Entry;
import org.opends.server.types.Modification;
import org.opends.server.util.TimeThread;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/** Test AttrHistoricalMultiple. */
@SuppressWarnings("javadoc")
public class AttrHistoricalMultipleTest extends ReplicationTestCase
{
  private static final String ATTRIBUTE_NAME = "description";
  private static final boolean CONFLICT = true;
  private static final boolean SUCCESS = false;

  private CSNGenerator csnGen = new CSNGenerator(1025, System.currentTimeMillis());
  private AttrHistoricalMultiple attrHist;
  private CSN csn;
  private Entry entry;
  /** Avoids declaring the variable in the tests. */
  private Modification mod;

  @BeforeMethod
  public void localSetUp() throws Exception
  {
    attrHist = new AttrHistoricalMultiple();
    csn = csnGen.newCSN();
    entry = new Entry(null, null, null, null);
    entry = TestCaseUtils.makeEntry(
        "dn: uid=test.user",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password");
  }

  @AfterMethod
  public void localTearDown() throws Exception
  {
    attrHist = null;
    csn = null;
  }

  /** Build some data for the AttrInfo test below. */
  @DataProvider(name = "attrInfo")
  public Object[][] createData()
  {
    ByteString att1 = ByteString.valueOfUtf8("string");
    ByteString att2 = ByteString.valueOfUtf8("value");
    ByteString att3 = ByteString.valueOfUtf8("again");

    CSN del1 = new CSN(1,  0,  1);
    CSN del2 = new CSN(1,  1,  1);
    CSN del3 = new CSN(1,  0,  2);

    CSN upd1 = new CSN(TimeThread.getTime(), 123, 45);
    CSN upd2 = new CSN(TimeThread.getTime() + 1000, 123,  45);
    CSN upd3 = new CSN(TimeThread.getTime(), 321, 54);

    return new Object[][]
    {
    { att1, del1, upd1 },
    { att2, del2, upd2 },
    { att3, del3, upd3 },
    { att3, upd3, upd3 } };
  }

  /** Create a AttrInfo and check the methods. */
  @Test(dataProvider = "attrInfo")
  public void attrInfo(ByteString att, CSN deleteTime, CSN updateTime) throws Exception
  {
    // Create an empty AttrInfo
    AttrHistoricalMultiple attrInfo1 = new AttrHistoricalMultiple();

    // Check
    attrInfo1.add(att, updateTime);
    Set<AttrValueHistorical> values1 = attrInfo1.getValuesHistorical();
    assertEquals(values1.size(), 1);
    AttrValueHistorical valueInfo1 = new AttrValueHistorical(att, updateTime, null);
    assertTrue(values1.contains(valueInfo1));

    // Check constructor with parameter
    AttrValueHistorical valueInfo2 = new AttrValueHistorical(att, updateTime, deleteTime);
    AttrHistoricalMultiple attrInfo2 = new AttrHistoricalMultiple(
        deleteTime, updateTime, Collections.singletonMap(valueInfo2, valueInfo2));

    // Check equality
    //assertTrue(attrInfo1.getDeleteTime().compareTo(attrInfo2.getDeleteTime())==0);

    //  Check constructor with time parameter and not Value
    AttrHistoricalMultiple attrInfo3 = new AttrHistoricalMultiple(deleteTime, updateTime, null);
    attrInfo3.add(att, updateTime);
    Set<AttrValueHistorical> values3 = attrInfo3.getValuesHistorical();
    assertEquals(values3.size(), 1);
    valueInfo1 = new AttrValueHistorical(att, updateTime, null);
    assertTrue(values3.contains(valueInfo1));

    // Check duplicate
    AttrHistoricalMultiple attrInfo4 = attrInfo3.duplicate();
    Set<AttrValueHistorical> values4 = attrInfo4.getValuesHistorical();
    assertEquals(attrInfo4.getDeleteTime().compareTo(attrInfo3.getDeleteTime()), 0);
    assertEquals(values4.size(), values3.size());

    // Check
    attrInfo4.delete(att, updateTime);
    assertEquals(attrInfo4.getValuesHistorical().size(), 1);

    // Check
    AttributeType type = DirectoryServer.getAttributeTypeOrNull(ATTRIBUTE_NAME);
    attrInfo3.delete(Attributes.create(type, att), updateTime) ;
    assertEquals(attrInfo3.getValuesHistorical().size(), 1);

    // Check
    attrInfo2.delete(updateTime);
    assertEquals(attrInfo2.getValuesHistorical().size(), 0);
  }

  @Test
  public void replay_addDeleteSameTime() throws Exception
  {
    mod = newModification(ADD, "X");
    replayOperation(csn, entry, mod, SUCCESS);
    assertAttributeValues(entry, "X");

    mod = newModification(DELETE, "X");
    replayOperation(csn, entry, mod, SUCCESS);
    assertNoAttributeValue(entry);
  }

  @Test
  public void replay_addThenAddThenOlderDelete() throws Exception
  {
    CSN[] t = newCSNs(3);

    mod = newModification(ADD, "X");
    replayOperation(t[0], entry, mod, SUCCESS);
    assertAttributeValues(entry, "X");

    mod = newModification(ADD, "Y");
    replayOperation(t[2], entry, mod, SUCCESS);
    assertAttributeValues(entry, "X", "Y");

    mod = newModification(DELETE, "Y");
    replayOperationSuppressMod(t[1], entry, mod, CONFLICT);
    assertAttributeValues(entry, "X", "Y");
  }

  @Test
  public void replay_addThenDeleteNoValueThenOlderAdd() throws Exception
  {
    CSN[] t = newCSNs(3);

    replay_addDeleteNoValue(t[0], t[2]);

    mod = newModification(ADD, "Y");
    replayOperationSuppressMod(t[1], entry, mod, CONFLICT);
    assertNoAttributeValue(entry);
  }

  @Test
  public void replay_addThenDeleteWithValueThenOlderAdd() throws Exception
  {
    CSN[] t = newCSNs(3);

    mod = newModification(ADD, "X");
    replayOperation(t[0], entry, mod, SUCCESS);
    assertAttributeValues(entry, "X");

    mod = newModification(DELETE, "X");
    replayOperation(t[2], entry, mod, SUCCESS);
    assertNoAttributeValue(entry);

    mod = newModification(ADD, "X");
    replayOperationSuppressMod(t[1], entry, mod, CONFLICT);
    assertNoAttributeValue(entry);
  }

  @Test
  public void replay_addThenAdd() throws Exception
  {
    CSN[] t = newCSNs(2);

    mod = newModification(ADD, "X");
    replayOperation(t[0], entry, mod, SUCCESS);
    assertAttributeValues(entry, "X");

    mod = newModification(ADD, "X");
    replayOperationSuppressMod(t[1], entry, mod, CONFLICT);
    assertAttributeValues(entry, "X");
  }

  @Test
  public void replay_addThenDeleteThenAdd() throws Exception
  {
    CSN[] t = newCSNs(3);

    mod = newModification(ADD, "X");
    replayOperation(t[0], entry, mod, SUCCESS);
    assertAttributeValues(entry, "X");

    mod = newModification(DELETE, "X");
    replayOperation(t[1], entry, mod, SUCCESS);
    assertNoAttributeValue(entry);

    mod = newModification(ADD, "X");
    replayOperation(t[1], entry, mod, SUCCESS);
    assertAttributeValues(entry, "X");
  }

  @Test
  public void replay_deleteNoPreviousHistory() throws Exception
  {
    mod = newModification(DELETE, "Y");
    replayOperationSuppressMod(csn, entry, mod, CONFLICT);
    assertNoAttributeValue(entry);
  }

  @Test
  public void replay_addThenDelete() throws Exception
  {
    CSN[] t = newCSNs(2);

    mod = newModification(ADD, "X");
    replayOperation(t[0], entry, mod, SUCCESS);
    assertAttributeValues(entry, "X");

    mod = newModification(DELETE, "X");
    replayOperation(t[1], entry, mod, SUCCESS);
    assertNoAttributeValue(entry);
  }

  @Test
  public void replay_addThenDeleteThenOlderDelete() throws Exception
  {
    CSN[] t = newCSNs(3);

    replay_addDeleteNoValue(t[0], t[2]);

    mod = newModification(DELETE, "X");
    replayOperationSuppressMod(t[1], entry, mod, CONFLICT);
    assertNoAttributeValue(entry);
  }

  @Test
  public void replay_addDeleteNoValueSameTimeNotConflict() throws Exception
  {
    replay_addDeleteNoValue(csn, csn);
  }

  @Test
  public void replay_addThenDeleteNoValue() throws Exception
  {
    CSN[] t = newCSNs(2);
    replay_addDeleteNoValue(t[0], t[1]);
  }

  private void replay_addDeleteNoValue(CSN tAdd, CSN tDel) throws Exception
  {
    mod = newModification(ADD, "X");
    replayOperation(tAdd, entry, mod, SUCCESS);
    assertAttributeValues(entry, "X");

    mod = newModification(DELETE);
    replayOperation(tDel, entry, mod, SUCCESS);
    assertNoAttributeValue(entry);
  }

  @Test
  public void replay_replace() throws Exception
  {
    mod = newModification(REPLACE, "X");
    replayOperation(csn, entry, mod, SUCCESS);
    assertAttributeValues(entry, "X");
  }

  @Test
  public void replay_addThenOlderReplace() throws Exception
  {
    CSN[] t = newCSNs(2);

    mod = newModification(ADD, "X");
    replayOperation(t[1], entry, mod, SUCCESS);
    assertAttributeValues(entry, "X");

    mod = newModification(REPLACE, "Y");
    replayOperation(t[0], entry, mod, SUCCESS);
    assertAttributeValues(entry, "X", "Y");
  }

  @Test
  public void replay_addThenDeleteThenOlderReplace() throws Exception
  {
    CSN[] t = newCSNs(3);

    replay_addDeleteNoValue(t[0], t[2]);

    mod = newModification(REPLACE, "Y");
    replayOperationSuppressMod(t[1], entry, mod, CONFLICT);
    assertNoAttributeValue(entry);
  }

  private CSN[] newCSNs(int nb)
  {
    CSN[] results = new CSN[nb];
    for (int i = 0; i < nb; i++)
    {
      results[i] = csnGen.newCSN();
    }
    return results;
  }

  private Modification newModification(ModificationType modType, String attrValue)
  {
    return new Modification(modType, Attributes.create(ATTRIBUTE_NAME, attrValue));
  }

  private Modification newModification(ModificationType modType)
  {
    return new Modification(modType, Attributes.empty(ATTRIBUTE_NAME));
  }

  private void replayOperationSuppressMod(CSN csn, Entry entry, Modification mod, boolean shouldConflict)
      throws Exception
  {
    Iterator<Modification> itMod = mock(Iterator.class);
    replayOperation(itMod, csn, entry, mod, shouldConflict);
    verifyModSuppressed(itMod);
  }

  private void replayOperation(CSN csn, Entry entry, Modification mod, boolean shouldConflict) throws Exception
  {
    Iterator<Modification> itMod = mock(Iterator.class);
    replayOperation(itMod, csn, entry, mod, shouldConflict);
    verifyZeroInteractions(itMod);
  }

  private void replayOperation(Iterator<Modification> modsIterator, CSN csn, Entry entry, Modification mod,
      boolean shouldConflict) throws Exception
  {
    boolean result = attrHist.replayOperation(modsIterator, csn, entry, mod);
    assertEquals(result, shouldConflict,
        "Expected " + (shouldConflict ? "a" : "no") + " conflict when applying " + mod + " to " + entry);
    if (entry != null && !shouldConflict)
    {
      entry.applyModification(mod);
      assertAttributeValues(entry, mod);
      conformsToSchema(entry);
    }
  }

  private void assertAttributeValues(Entry entry, Modification mod)
  {
    List<ByteString> actualValues = getValues(entry, mod);
    List<ByteString> expectedValues = getValues(mod.getAttribute());
    switch (mod.getModificationType().asEnum())
    {
    case ADD:
      assertThat(actualValues).containsAll(expectedValues);
      return;

    case REPLACE:
      assertThat(actualValues).isEqualTo(expectedValues);
      return;

    case DELETE:
      if (expectedValues.isEmpty())
      {
        assertThat(actualValues).isEmpty();
      }
      else
      {
        assertThat(actualValues).doesNotContainAnyElementsOf(expectedValues);
      }
      return;

    case INCREMENT:
      return;
    }
  }

  private List<ByteString> getValues(Entry entry, Modification mod)
  {
    return getValues(entry.getAttribute(mod.getAttribute().getAttributeType()));
  }

  private List<ByteString> getValues(List<Attribute> attributes)
  {
    if (attributes != null)
    {
      assertThat(attributes).hasSize(1);
      return getValues(attributes.get(0));
    }
    return Collections.emptyList();
  }

  private List<ByteString> getValues(Attribute attribute)
  {
    List<ByteString> results = new ArrayList<>();
    for (ByteString value : attribute)
    {
      results.add(value);
    }
    return results;
  }

  private void conformsToSchema(Entry entry)
  {
    LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
    final boolean isValid = entry.conformsToSchema(null, false, false, false, invalidReason);
    assertThat(isValid).as(invalidReason.toString()).isTrue();
  }

  private void assertNoAttributeValue(Entry entry)
  {
    assertAttributeValues(entry);
  }

  private void assertAttributeValues(Entry entry, String... expectedValues)
  {
    List<ByteString> actualValues = getValues(entry.getAttribute(ATTRIBUTE_NAME));
    assertThat(actualValues).containsOnly(toByteStrings(expectedValues));
  }

  private ByteString[] toByteStrings(String... strings)
  {
    ByteString[] results = new ByteString[strings.length];
    for (int i = 0; i < results.length; i++)
    {
      results[i] = ByteString.valueOfUtf8(strings[i]);
    }
    return results;
  }

  private void verifyModSuppressed(Iterator<Modification> it)
  {
    verify(it, times(1)).remove();
    verify(it, only()).remove();
  }
}
