/**
 * Copyright (c) 2014 CEA and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 *
 * Contributors:
 *   CEA - Initial API and implementation
 */
package org.eclipse.emf.test.core.ecore;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EGenericType;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.ETypedElement;
import org.eclipse.emf.ecore.EcoreFactory;
import org.eclipse.emf.ecore.EcorePackage;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceImpl;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.EContentsEList;
import org.eclipse.emf.ecore.util.ECrossReferenceAdapter;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl;
import org.eclipse.emf.test.common.TestUtil;
import org.eclipse.emf.test.core.AllSuites;
import org.eclipse.emf.test.core.xrefsmodel.A;
import org.eclipse.emf.test.core.xrefsmodel.XRefsModelPackage;
import org.eclipse.emf.test.core.xrefsmodel.util.XRefsModelUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;


@RunWith(Parameterized.class)
public class ECrossReferenceAdapterTest
{
  @Parameters(name="{0}")
  public static Collection<ECrossReferenceAdapterFixture> eCrossReferenceAdapters()
  {
    return Arrays.asList(new ECrossReferenceAdapterFixture(), new IterativeECrossReferenceAdapterFixture());
  }

  private EPackage testPackage;

  private EAttribute mapAttribute;

  @Parameter
  public ECrossReferenceAdapterFixture fixture;

  /**
   * Tests the filtered intrinsic cross-reference iterator for a resolving cross-referencer.
   */
  @Test
  public void testCrossReferenceIterator_resolving()
  {
    A a0 = loadXRefsInstance();

    List<EStructuralFeature> expectedEntries = new ArrayList<EStructuralFeature>(
      Arrays.asList(XRefsModelPackage.Literals.A__OTHERS, XRefsModelPackage.Literals.A__OTHERS, XRefsModelPackage.Literals.A__NON_OTHERS));
    List<String> expectedNames = new ArrayList<String>(Arrays.asList("a1", "a2", "a3"));
    for (EContentsEList.FeatureIterator<EObject> iter = fixture.getCrossReferences(a0); iter.hasNext();)
    {
      A next = (A)iter.next();
      EStructuralFeature feature = iter.feature();

      // Assert that we don't get an unexpected entry
      assertTrue(expectedEntries.remove(feature));

      // Assert that we get the expected object
      assertFalse(next.eIsProxy());
      assertEquals(expectedNames.remove(0), next.getName());
    }

    // Assert that all of the expected entries were found
    assertTrue(expectedEntries.isEmpty());
  }

  /**
   * Tests the filtered intrinsic cross-reference iterator for a non-resolving cross-referencer.
   */
  @Test
  public void testCrossReferenceIterator_nonResolving()
  {
    // Don't resolve cross-references
    fixture.setResolve(false);

    A a0 = loadXRefsInstance();

    List<EStructuralFeature> expectedEntries = new ArrayList<EStructuralFeature>(
      Arrays.asList(XRefsModelPackage.Literals.A__OTHERS, XRefsModelPackage.Literals.A__OTHERS, XRefsModelPackage.Literals.A__NON_OTHERS));
    List<String> expectedURIs = new ArrayList<String>(Arrays.asList("xrefs1.xmi#/0", "xrefs1.xmi#/1", "xrefs1.xmi#/2"));
    for (EContentsEList.FeatureIterator<EObject> iter = fixture.getCrossReferences(a0); iter.hasNext();)
    {
      A next = (A)iter.next();
      EStructuralFeature feature = iter.feature();

      // Assert that we don't get an unexpected entry
      assertTrue(expectedEntries.remove(feature));

      // Assert that we get the expected object
      assertTrue(next.eIsProxy());
      assertEquals(expectedURIs.remove(0), EcoreUtil.getURI(next).deresolve(a0.eResource().getURI()).toString());
    }

    // Assert that all of the expected entries were found
    assertTrue(expectedEntries.isEmpty());
  }

  /**
   * Tests the wrapped intrinsic cross-reference iterator for a resolving cross-referencer. This also tests that
   * its implementation of {@link org.eclipse.emf.ecore.util.EContentsEList.FeatureIterator#feature()} is consistent.
   */
  @Test
  public void testCrossReferenceIterator_resolving_wrapper()
  {
    XRefsModelUtil.setWrapCrossReferenceIterators(true);

    A a0 = loadXRefsInstance();

    List<EStructuralFeature> expectedEntries = new ArrayList<EStructuralFeature>(
      Arrays.asList(XRefsModelPackage.Literals.A__OTHERS, XRefsModelPackage.Literals.A__OTHERS, XRefsModelPackage.Literals.A__NON_OTHERS));
    List<String> expectedNames = new ArrayList<String>(Arrays.asList("a1", "a2", "a3"));
    for (EContentsEList.FeatureIterator<EObject> iter = fixture.getCrossReferences(a0); iter.hasNext();)
    {
      A next = (A)iter.next();
      EStructuralFeature feature = iter.feature();

      // Assert that we don't get an unexpected entry
      assertTrue(expectedEntries.remove(feature));

      // Assert that we get the expected object
      assertFalse(next.eIsProxy());
      assertEquals(expectedNames.remove(0), next.getName());
    }

    // Assert that all of the expected entries were found
    assertTrue(expectedEntries.isEmpty());

    // And that we couldn't avoid calling the derived A::getAllOthers() exactly twice:
    // once for eIsSet() and once for eGet()
    assertEquals(2, XRefsModelUtil.getAllOthersCallCount());
  }

  /**
   * Tests the wrapped intrinsic cross-reference iterator for a non-resolving cross-referencer. This also tests that
   * its implementation of {@link org.eclipse.emf.ecore.util.EContentsEList.FeatureIterator#feature()} is consistent.
   */
  @Test
  public void testCrossReferenceIterator_nonResolving_wrapper()
  {
    XRefsModelUtil.setWrapCrossReferenceIterators(true);

    // Don't resolve cross-references
    fixture.setResolve(false);

    A a0 = loadXRefsInstance();

    List<EStructuralFeature> expectedEntries = new ArrayList<EStructuralFeature>(
      Arrays.asList(XRefsModelPackage.Literals.A__OTHERS, XRefsModelPackage.Literals.A__OTHERS, XRefsModelPackage.Literals.A__NON_OTHERS));
    List<String> expectedURIs = new ArrayList<String>(Arrays.asList("xrefs1.xmi#/0", "xrefs1.xmi#/1", "xrefs1.xmi#/2"));
    for (EContentsEList.FeatureIterator<EObject> iter = fixture.getCrossReferences(a0); iter.hasNext();)
    {
      A next = (A)iter.next();
      EStructuralFeature feature = iter.feature();

      // Assert that we don't get an unexpected entry
      assertTrue(expectedEntries.remove(feature));

      // Assert that we get the expected object
      assertTrue(next.eIsProxy());
      assertEquals(expectedURIs.remove(0), EcoreUtil.getURI(next).deresolve(a0.eResource().getURI()).toString());
    }

    // Assert that all of the expected entries were found
    assertTrue(expectedEntries.isEmpty());

    // And that we couldn't avoid calling the derived A::getAllOthers() exactly twice:
    // once for eIsSet() and once for eGet()
    assertEquals(2, XRefsModelUtil.getAllOthersCallCount());
  }

  /**
   * Control test case: ETypedElements referencing types simply by eType.
   */
  @Test
  public void testSimpleTypeTypeReferencesDoNotLeak()
  {
    Resource resource = new ResourceImpl(URI.createURI("http:///bogus/testpackage.ecore"));
    resource.getContents().add(testPackage);
    resource.eAdapters().add(fixture);
    EcoreUtil.resolveAll(resource);

    // sanity check
    assertSame(EcorePackage.Literals.ESTRING, mapAttribute.getEType());

    fixture.assertCrossReferenceMapNotEmpty();
    fixture.assertCrossReferenced(mapAttribute, EcorePackage.Literals.ESTRING);

    resource.unload();

    fixture.assertCrossReferenceMapEmpty();
  }

  /**
   * Memory leak scenario:  ETypedElements referencing types via eGenericType
   *
   * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=433027">https://bugs.eclipse.org/bugs/show_bug.cgi?id=433027</a>
   */
  @Test
  public void testGenericTypeTypeReferencesDoNotLeak()
  {
    // change the map attribute to a generic type
    mapAttribute.setEType(null);
    EGenericType genType = EcoreFactory.eINSTANCE.createEGenericType();
    mapAttribute.setEGenericType(genType);
    genType.setEClassifier(EcorePackage.Literals.EMAP);
    EGenericType k = EcoreFactory.eINSTANCE.createEGenericType();
    genType.getETypeArguments().add(k);
    k.setEClassifier(EcorePackage.Literals.EJAVA_OBJECT);
    EGenericType t = EcoreFactory.eINSTANCE.createEGenericType();
    genType.getETypeArguments().add(t);
    t.setEClassifier(EcorePackage.Literals.EJAVA_OBJECT);

    Resource resource = new ResourceImpl(URI.createURI("http:///bogus/testpackage.ecore"));
    resource.getContents().add(testPackage);
    resource.eAdapters().add(fixture);
    EcoreUtil.resolveAll(resource);

    // sanity check
    assertSame(EcorePackage.Literals.EMAP, mapAttribute.getEType());

    fixture.assertCrossReferenceMapNotEmpty();
    fixture.assertCrossReferenced(mapAttribute, EcorePackage.Literals.EMAP);

    resource.unload();

    fixture.assertCrossReferenceMapEmpty();
  }

  //
  // Test framework
  //

  @Before
  public void setUp()
  {
    // We must never call the A::getAllOthers() derived reference accessor when cross-referencing
    XRefsModelUtil.assertNoAllOthersCalls(true);

    testPackage = EcoreFactory.eINSTANCE.createEPackage();
    testPackage.setName("test");
    testPackage.setNsPrefix("test");
    testPackage.setNsURI("http://testpackage");

    EClass foo = EcoreFactory.eINSTANCE.createEClass();
    testPackage.getEClassifiers().add(foo);
    foo.setName("Foo");

    mapAttribute = EcoreFactory.eINSTANCE.createEAttribute();
    foo.getEStructuralFeatures().add(mapAttribute);
    mapAttribute.setName("map");
    mapAttribute.setEType(EcorePackage.Literals.ESTRING);

    // Create a fresh new instance the same as the parameter.
    fixture = fixture.newInstance();
  }

  @After
  public void tearDown()
  {
    XRefsModelUtil.assertNoAllOthersCalls(false);

    // In case it was set by a test case
    XRefsModelUtil.setWrapCrossReferenceIterators(false);

    testPackage = null;
    mapAttribute = null;
    fixture = null;
  }

  private A loadXRefsInstance()
  {
    ResourceSet rset = new ResourceSetImpl();
    rset.getResourceFactoryRegistry().getExtensionToFactoryMap().put("xmi", new XMIResourceFactoryImpl());
    rset.getPackageRegistry().put(XRefsModelPackage.eNS_URI, XRefsModelPackage.eINSTANCE);
    Resource resource = rset.getResource(URI.createFileURI(TestUtil.getPluginDirectory(AllSuites.PLUGIN_ID) + "/data/xrefs0.xmi"), true);
    return (A)resource.getContents().get(0);
  }

  static class ECrossReferenceAdapterFixture extends ECrossReferenceAdapter
  {
    private boolean resolve = true;

    class InverseCrossReferencerFixture extends InverseCrossReferencer
    {
      private static final long serialVersionUID = 1L;

      @Override
      protected EContentsEList.FeatureIterator<EObject> getCrossReferences(EObject eObject)
      {
        return super.getCrossReferences(eObject);
      }
    }

    @Override
    protected InverseCrossReferencer createInverseCrossReferencer()
    {
      return new InverseCrossReferencerFixture();
    }

    EContentsEList.FeatureIterator<EObject> getCrossReferences(EObject eObject)
    {
      return ((InverseCrossReferencerFixture)inverseCrossReferencer).getCrossReferences(eObject);
    }

    void setResolve(boolean resolve)
    {
      this.resolve = resolve;
    }

    @Override
    protected boolean resolve()
    {
      return resolve;
    }

    void assertCrossReferenced(ETypedElement typedElement, EClassifier type)
    {
      Collection<EStructuralFeature.Setting> settings = getInverseReferences(type);
      for (EStructuralFeature.Setting next : settings)
      {
        if ((next.getEObject() == typedElement) && (next.getEStructuralFeature() == EcorePackage.Literals.ETYPED_ELEMENT__ETYPE))
        {
          return;
        }
      }

      fail("Cross-reference not found for ETypedElement::eType");
    }

    void assertCrossReferenceMapNotEmpty()
    {
      assertFalse("Cross-reference map is empty", inverseCrossReferencer.isEmpty());
    }

    void assertCrossReferenceMapEmpty()
    {
      assertTrue("Cross-reference map is not empty", inverseCrossReferencer.isEmpty());
    }

    public ECrossReferenceAdapterFixture newInstance()
    {
      return new ECrossReferenceAdapterFixture();
    }

    @Override
    public String toString()
    {
      return "recursive";
    }
  }

  static class IterativeECrossReferenceAdapterFixture extends ECrossReferenceAdapterFixture
  {
    @Override
    protected boolean useRecursion()
    {
      return false;
    }

    @Override
    public IterativeECrossReferenceAdapterFixture newInstance()
    {
      return new IterativeECrossReferenceAdapterFixture();
    }
    
    @Override
    public String toString()
    {
      return "iterative";
    }
  }
}
