/*
 * 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 2015 ForgeRock AS
 *
 *      Portions Copyrighted 2019 OGIS-RI Co., Ltd.
 */
package org.opends.server.replication.plugin;

import static org.forgerock.opendj.ldap.ModificationType.*;
import static org.mockito.Mockito.*;
import static org.opends.server.util.CollectionUtils.*;

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

import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ModificationType;
import org.opends.server.TestCaseUtils;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.common.CSN;
import org.opends.server.replication.protocol.ModifyContext;
import org.opends.server.replication.protocol.OperationContext;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.Attributes;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.Modification;
import org.opends.server.types.operation.PreOperationModifyOperation;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * Tests the single valued attribute conflict resolution (part of modify replay).
 * <p>
 * It produces series of changes and replay them out of order by generating all possible
 * permutations. The goal is to end up in the same final state whatever the order.
 * <p>
 * The tests are built this way:
 * <ol>
 * <li>Start from an entry without (resp. with) an initial value for the targeted single valued
 * attribute</li>
 * <li>Replay a "seed" modify operation, that happened at time t1</li>
 * <li>Then replay any of all the possible operations that could come after, that happened at time
 * t2</li>
 * </ol>
 * All permutations for these sequence of operations are tested.
 * <p>
 * The test finally asserts the {@code ds-sync-hist} attribute always end in the same state whatever
 * the permutation.
 */
@SuppressWarnings("javadoc")
public class ModifyReplaySingleValuedAttributeTest extends ReplicationTestCase
{
  private static final String ATTRIBUTE_NAME = "displayName";
  private static final String SYNCHIST = "ds-sync-hist";

  private Entry entry;

  private static class Mod
  {
    private final int time;
    private final Modification modification;

    private Mod(ModificationType modType, int t)
    {
      this(modType, null, t);
    }

    private Mod(ModificationType modType, String value, int t)
    {
      this.modification = newModification(modType, value);
      this.time = t;
    }

    private PreOperationModifyOperation toOperation()
    {
      final ModifyContext value = new ModifyContext(new CSN(0, time, 0), null);

      PreOperationModifyOperation op = mock(PreOperationModifyOperation.class);
      when(op.getModifications()).thenReturn(newArrayList(modification));
      when(op.getAttachment(eq(OperationContext.SYNCHROCONTEXT))).thenReturn(value);
      return op;
    }

    /** Implemented to get a nice display for each tests in Eclipse UI. */
    @Override
    public String toString()
    {
      String modType = modification.getModificationType().toString().toUpperCase();
      Iterator<ByteString> it = modification.getAttribute().iterator();
      String attrValue = it.hasNext() ? "\"" + it.next().toString() + "\" " : "";
      return modType + " " + attrValue + "t" + time;
    }
  }

  private static Object[][] generatePermutations(Object[][] scenarios)
  {
    List<Object[]> results = new ArrayList<Object[]>();
    for (Object[] scenario : scenarios)
    {
      generate((Object[]) scenario[0], (Attribute) scenario[1], results);
    }

    return results.toArray(new Object[results.size()][]);
  }

  private static void generate(Object[] array, Attribute dsSyncHist, List<Object[]> results)
  {
    generate(array.length, array, dsSyncHist, results);
  }

  private static void generate(int n, Object[] array, Attribute dsSyncHist, List<Object[]> results)
  {
    if (n == 1)
    {
      results.add(new Object[] { Arrays.asList(Arrays.copyOf(array, array.length)), dsSyncHist, });
      return;
    }

    for (int i = 0; i < n - 1; i += 1)
    {
      generate(n - 1, array, dsSyncHist, results);
      if (n % 2 == 0)
      {
        swap(array, i, n - 1);
      }
      else
      {
        swap(array, 0, n - 1);
      }
    }
    generate(n - 1, array, dsSyncHist, results);
  }

  private static <E> void swap(E[] array, int i, int j)
  {
    E tmp = array[i];
    array[i] = array[j];
    array[j] = tmp;
  }

  @DataProvider
  public Object[][] add_data()
  {
    // @formatter:off
    return generatePermutations(new Object[][] {
      { mods(new Mod(ADD, "X", 1)),                                 dsSyncHist(1, ":add:X"), },
      { mods(new Mod(ADD, "X", 1), new Mod(ADD, "X", 2)),           dsSyncHist(2, ":add:X"), },
      { mods(new Mod(ADD, "X", 1), new Mod(ADD, "Y", 2)),           dsSyncHist(2, ":add:Y"), },
      { mods(new Mod(ADD, "X", 1), new Mod(DELETE, "X", 2)),        dsSyncHist(2, ":attrDel"), },
      { mods(new Mod(ADD, "X", 1), new Mod(DELETE, "Y", 2)),        dsSyncHist(1, ":add:X"), },
      { mods(new Mod(ADD, "X", 1), new Mod(DELETE, 2)),             dsSyncHist(2, ":attrDel"), },
      { mods(new Mod(ADD, "X", 1), new Mod(REPLACE, "X", 2)),       dsSyncHist(2, ":repl:X"), },
      { mods(new Mod(ADD, "X", 1), new Mod(REPLACE, "Y", 2)),       dsSyncHist(2, ":repl:Y"), },
      { mods(new Mod(ADD, "X", 1), new Mod(REPLACE, 2)),            dsSyncHist(2, ":attrDel"), }
    });
    // @formatter:on
  }

  @DataProvider
  public Object[][] delete_noInitialValue_data()
  {
    // @formatter:off
    return generatePermutations(new Object[][] {
        { mods(new Mod(DELETE, "X", 1)),                            dsSyncHist(1, ":attrDel"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(ADD, "X", 2)),      dsSyncHist(2, ":add:X"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(ADD, "Y", 2)),      dsSyncHist(2, ":add:Y"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(DELETE, "X", 2)),   dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(DELETE, "Y", 2)),   dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(DELETE, 2)),        dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(REPLACE, "X", 2)),  dsSyncHist(2, ":repl:X"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(REPLACE, "Y", 2)),  dsSyncHist(2, ":repl:Y"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(REPLACE, 2)),       dsSyncHist(2, ":attrDel"), },

        { mods(new Mod(DELETE, 1)),                                 dsSyncHist(1, ":attrDel"), },
        { mods(new Mod(DELETE, 1), new Mod(ADD, "X", 2)),           dsSyncHist(2, ":add:X"), },
        { mods(new Mod(DELETE, 1), new Mod(ADD, "Y", 2)),           dsSyncHist(2, ":add:Y"), },
        { mods(new Mod(DELETE, 1), new Mod(DELETE, "X", 2)),        dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, 1), new Mod(DELETE, "Y", 2)),        dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, 1), new Mod(DELETE, 2)),             dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, 1), new Mod(REPLACE, "X", 2)),       dsSyncHist(2, ":repl:X"), },
        { mods(new Mod(DELETE, 1), new Mod(REPLACE, "Y", 2)),       dsSyncHist(2, ":repl:Y"), },
        { mods(new Mod(DELETE, 1), new Mod(REPLACE, 2)),            dsSyncHist(2, ":attrDel"), },
    });
    // @formatter:on
  }

  @DataProvider
  public Object[][] delete_initialValueX_data()
  {
    // @formatter:off
    return generatePermutations(new Object[][] {
        { mods(new Mod(DELETE, "X", 1)),                            dsSyncHist(1, ":attrDel"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(ADD, "X", 2)),      dsSyncHist(2, ":add:X"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(ADD, "Y", 2)),      dsSyncHist(2, ":add:Y"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(DELETE, "X", 2)),   dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(DELETE, "Y", 2)),   dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(DELETE, 2)),        dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(REPLACE, "X", 2)),  dsSyncHist(2, ":repl:X"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(REPLACE, "Y", 2)),  dsSyncHist(2, ":repl:Y"), },
        { mods(new Mod(DELETE, "X", 1), new Mod(REPLACE, 2)),       dsSyncHist(2, ":attrDel"), },

        { mods(new Mod(DELETE, "Y", 1)),                            dsSyncHist(1, ":add:X"), },
        { mods(new Mod(DELETE, "Y", 1), new Mod(ADD, "X", 2)),      dsSyncHist(1, ":add:X"), },
        { mods(new Mod(DELETE, "Y", 1), new Mod(ADD, "Y", 2)),      dsSyncHist(1, ":add:X"), },
        { mods(new Mod(DELETE, "Y", 1), new Mod(DELETE, "X", 2)),   dsSyncHist(1, ":add:X"), },
        { mods(new Mod(DELETE, "Y", 1), new Mod(DELETE, "Y", 2)),   dsSyncHist(1, ":add:X"), },
        { mods(new Mod(DELETE, "Y", 1), new Mod(DELETE, 2)),        dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, "Y", 1), new Mod(REPLACE, "X", 2)),  dsSyncHist(1, ":add:X"), },
        { mods(new Mod(DELETE, "Y", 1), new Mod(REPLACE, "Y", 2)),  dsSyncHist(1, ":add:X"), },
        { mods(new Mod(DELETE, "Y", 1), new Mod(REPLACE, 2)),       dsSyncHist(2, ":attrDel"), },

        { mods(new Mod(DELETE, 1)),                                 dsSyncHist(1, ":attrDel"), },
        { mods(new Mod(DELETE, 1), new Mod(ADD, "X", 2)),           dsSyncHist(2, ":add:X"), },
        { mods(new Mod(DELETE, 1), new Mod(ADD, "Y", 2)),           dsSyncHist(2, ":add:Y"), },
        { mods(new Mod(DELETE, 1), new Mod(DELETE, "X", 2)),        dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, 1), new Mod(DELETE, "Y", 2)),        dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, 1), new Mod(DELETE, 2)),             dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(DELETE, 1), new Mod(REPLACE, "X", 2)),       dsSyncHist(2, ":repl:X"), },
        { mods(new Mod(DELETE, 1), new Mod(REPLACE, "Y", 2)),       dsSyncHist(2, ":repl:Y"), },
        { mods(new Mod(DELETE, 1), new Mod(REPLACE, 2)),            dsSyncHist(2, ":attrDel"), },
    });
    // @formatter:on
  }

  @DataProvider
  public Object[][] replace_noInitialValue_data()
  {
    // @formatter:off
    return generatePermutations(new Object[][] {
        { mods(new Mod(REPLACE, "X", 1)),                           dsSyncHist(1, ":repl:X"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(ADD, "X", 2)),     dsSyncHist(2, ":add:X"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(ADD, "Y", 2)),     dsSyncHist(2, ":add:Y"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(DELETE, "X", 2)),  dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(DELETE, "Y", 2)),  dsSyncHist(1, ":repl:X"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(DELETE, 2)),       dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(REPLACE, 2)),      dsSyncHist(2, ":attrDel"), },

        { mods(new Mod(REPLACE, 1)),                                dsSyncHist(1, ":attrDel"), },
        { mods(new Mod(REPLACE, 1), new Mod(ADD, "X", 2)),          dsSyncHist(2, ":add:X"), },
        { mods(new Mod(REPLACE, 1), new Mod(ADD, "Y", 2)),          dsSyncHist(2, ":add:Y"), },
        { mods(new Mod(REPLACE, 1), new Mod(DELETE, "X", 2)),       dsSyncHist(1, ":add:X"), },
        { mods(new Mod(REPLACE, 1), new Mod(DELETE, "Y", 2)),       dsSyncHist(1, ":add:X"), },
        { mods(new Mod(REPLACE, 1), new Mod(DELETE, 2)),            dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(REPLACE, 1), new Mod(REPLACE, "X", 2)),      dsSyncHist(2, ":repl:X"), },
        { mods(new Mod(REPLACE, 1), new Mod(REPLACE, "Y", 2)),      dsSyncHist(2, ":repl:Y"), },
        { mods(new Mod(REPLACE, 1), new Mod(REPLACE, 2)),           dsSyncHist(2, ":attrDel"), },
    });
    // @formatter:on
  }

  @DataProvider
  public Object[][] replace_initialValueX_data()
  {
    // @formatter:off
    return generatePermutations(new Object[][] {
        { mods(new Mod(REPLACE, "X", 1)),                           dsSyncHist(1, ":repl:X"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(ADD, "X", 2)),     dsSyncHist(2, ":add:X"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(ADD, "Y", 2)),     dsSyncHist(2, ":add:Y"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(DELETE, "X", 2)),  dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(DELETE, "Y", 2)),  dsSyncHist(1, ":add:X"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(DELETE, 2)),       dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), },
        { mods(new Mod(REPLACE, "X", 1), new Mod(REPLACE, 2)),      dsSyncHist(2, ":attrDel"), },

        { mods(new Mod(REPLACE, "Y", 1)),                           dsSyncHist(1, ":repl:Y"), },
        { mods(new Mod(REPLACE, "Y", 1), new Mod(ADD, "X", 2)),     dsSyncHist(2, ":add:X"), },
        { mods(new Mod(REPLACE, "Y", 1), new Mod(ADD, "Y", 2)),     dsSyncHist(2, ":add:Y"), },
        { mods(new Mod(REPLACE, "Y", 1), new Mod(DELETE, "X", 2)),  dsSyncHist(1, ":repl:Y"), },
        { mods(new Mod(REPLACE, "Y", 1), new Mod(DELETE, "Y", 2)),  dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(REPLACE, "Y", 1), new Mod(DELETE, 2)),       dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(REPLACE, "Y", 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), },
        { mods(new Mod(REPLACE, "Y", 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), },
        { mods(new Mod(REPLACE, "Y", 1), new Mod(REPLACE, 2)),      dsSyncHist(2, ":attrDel"), },

        { mods(new Mod(REPLACE, 1)),                                dsSyncHist(1, ":attrDel"), },
        { mods(new Mod(REPLACE, 1), new Mod(ADD, "X", 2)),          dsSyncHist(2, ":add:X"), },
        { mods(new Mod(REPLACE, 1), new Mod(ADD, "Y", 2)),          dsSyncHist(2, ":add:Y"), },
        { mods(new Mod(REPLACE, 1), new Mod(DELETE, "X", 2)),       dsSyncHist(1, ":attrDel"), },
        { mods(new Mod(REPLACE, 1), new Mod(DELETE, "Y", 2)),       dsSyncHist(1, ":attrDel"), },
        { mods(new Mod(REPLACE, 1), new Mod(DELETE, 2)),            dsSyncHist(2, ":attrDel"), },
        { mods(new Mod(REPLACE, 1), new Mod(REPLACE, "X", 2)),      dsSyncHist(2, ":repl:X"), },
        { mods(new Mod(REPLACE, 1), new Mod(REPLACE, "Y", 2)),      dsSyncHist(2, ":repl:Y"), },
        { mods(new Mod(REPLACE, 1), new Mod(REPLACE, 2)),           dsSyncHist(2, ":attrDel"), },
    });
    // @formatter:on
  }

  @Test(dataProvider = "add_data", enabled = false)
  public void add_noInitialValue(List<Mod> mods, Attribute expectedDsSyncHist) throws Exception
  {
    noValue(); // also covers the initialValue("X"); case
    replay(mods, expectedDsSyncHist);
  }

  @Test(dataProvider = "delete_noInitialValue_data", enabled = false)
  public void delete_noInitialValue(List<Mod> mods, Attribute expectedDsSyncHist) throws Exception
  {
    noValue();
    replay(mods, expectedDsSyncHist);
  }

  @Test(dataProvider = "delete_initialValueX_data", enabled = false)
  public void delete_initialValueX(List<Mod> mods, Attribute expectedDsSyncHist) throws Exception
  {
    initialValue("X");
    replay(mods, expectedDsSyncHist);
  }

  @Test(dataProvider = "replace_noInitialValue_data", enabled = false)
  public void replace_noInitialValue(List<Mod> mods, Attribute expectedDsSyncHist) throws Exception
  {
    noValue();
    replay(mods, expectedDsSyncHist);
  }

  @Test(dataProvider = "replace_initialValueX_data", enabled = false)
  public void replace_initialValueX(List<Mod> mods, Attribute expectedDsSyncHist) throws Exception
  {
    initialValue("X");
    replay(mods, expectedDsSyncHist);
  }

  @Test(enabled = false)
  public void diffEntries_addThenDel() throws Exception
  {
    initialValue("X");

    replaySameTime(newArrayList(newModification(ADD, "Y"), newModification(DELETE, "X")), dsSyncHist(1, ":add:Y"));
  }

  @Test(enabled = false)
  public void diffEntries_delThenAdd() throws Exception
  {
    initialValue("X");

    replaySameTime(newArrayList(newModification(DELETE, "X"), newModification(ADD, "Y")), dsSyncHist(1, ":add:Y"));
  }

  private void replaySameTime(List<Modification> mods, Attribute expectedDsSyncHist) throws DirectoryException
  {
    final ModifyContext value = new ModifyContext(new CSN(0, 1, 0), null);

    PreOperationModifyOperation op = mock(PreOperationModifyOperation.class);
    when(op.getModifications()).thenReturn(mods);
    when(op.getAttachment(eq(OperationContext.SYNCHROCONTEXT))).thenReturn(value);

    EntryHistorical entryHistorical = EntryHistorical.newInstanceFromEntry(entry);
    entryHistorical.replayOperation(op, entry);
    entry.applyModification(new Modification(REPLACE, entryHistorical.encodeAndPurge()));

    AttributeType attrType = expectedDsSyncHist.getAttributeType();
    Attribute actual = entry.getExactAttribute(attrType, Collections.<String> emptySet());

    Assert.assertEquals(actual, expectedDsSyncHist, "wrong final value for ds-sync-hist attribute");
  }

  private void noValue() throws Exception
  {
    // @formatter:off
    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");
    // @formatter:on
  }

  private void initialValue(String attrValue) throws Exception
  {
    noValue();
    entry.applyModification(newModification(REPLACE, attrValue));
  }

  private void replay(List<Mod> mods, Attribute expectedDsSyncHist) throws DirectoryException
  {
    for (Mod op : mods)
    {
      EntryHistorical entryHistorical = EntryHistorical.newInstanceFromEntry(entry);
      entryHistorical.replayOperation(op.toOperation(), entry);
      entry.applyModification(new Modification(REPLACE, entryHistorical.encodeAndPurge()));
    }

    AttributeType attrType = expectedDsSyncHist.getAttributeType();
    Attribute actual = entry.getExactAttribute(attrType, Collections.<String> emptySet());

    Assert.assertEquals(actual, expectedDsSyncHist, "wrong final value for ds-sync-hist attribute");
  }

  private static Object[] mods(Mod... mods)
  {
    return mods;
  }

  private static Attribute dsSyncHist(int t, String partialDsSyncHist)
  {
    String value = ATTRIBUTE_NAME + ":000000000000000000000000000" + t + partialDsSyncHist;
    return Attributes.create(SYNCHIST, value);
  }

  private static Modification newModification(ModificationType modType, String value)
  {
    Attribute attr = value != null ? Attributes.create(ATTRIBUTE_NAME, value) : Attributes.empty(ATTRIBUTE_NAME);
    return new Modification(modType, attr);
  }
}
