/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2007, Geotools Project Managment Committee (PMC)
 *    
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.metadata;

import java.util.Collection;
import java.util.Set;
import java.util.HashSet;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.opengis.metadata.citation.CitationFactory;
import org.opengis.metadata.content.CoverageContentType;
import org.opengis.metadata.content.ImagingCondition;

import org.opengis.util.CodeList;
import org.opengis.metadata.MetaData;
import org.opengis.metadata.quality.Element;
import org.opengis.metadata.extent.VerticalExtent;
import org.opengis.metadata.citation.OnLineResource;
import org.opengis.metadata.maintenance.ScopeDescription;
import org.opengis.metadata.identification.AggregateInformation;
import org.opengis.metadata.identification.RepresentativeFraction;

import org.geotools.resources.Utilities;
import org.geotools.util.CheckedCollection;
import org.geotools.metadata.iso.MetaDataImpl;


/**
 * Tests every implementation in the {@link org.geotools.metadata.iso} package.
 *
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/test/java/org/geotools/metadata/ISOTest.java $
 * @version $Id: ISOTest.java 25193 2007-04-18 13:37:38Z desruisseaux $
 * @author Martin Desruisseaux (Geomatys)
 *
 * @todo Current implementation relies on {@link MetaData} dependencies. This is probably
 *       not enough; we should provide an explicit list of metadata interface.
 */
public class ISOTest extends TestCase {
    /**
     * {@code true} for displaying debugging informations.
     */
    private static final boolean VERBOSE = false;

    /**
     * Root package for interfaces, with trailing dot.
     */
    private static final String INTERFACE_PACKAGE = "org.opengis.metadata.";

    /**
     * Root package for implementations, with trailing dot.
     */
    private static final String IMPLEMENTATION_PACKAGE = "org.geotools.metadata.iso.";

    /**
     * Suffix for implementation classes.
     */
    private static final String IMPLEMENTATION_SUFFIX = "Impl";

    /**
     * List of GeoAPI interfaces to test. This list is not exclusive, since this test suite
     * will automatically scans for dependencies even if an interface do not appears in this
     * list. This list should not contains any {@link CodeList}.
     */
    private static final Class[] TEST = new Class[] {
        org.opengis.metadata.ApplicationSchemaInformation.class,
        org.opengis.metadata.ExtendedElementInformation.class,
        org.opengis.metadata.FeatureTypeList.class,
        org.opengis.metadata.Identifier.class,
        org.opengis.metadata.MetaData.class,
        org.opengis.metadata.MetadataExtensionInformation.class,
        org.opengis.metadata.PortrayalCatalogueReference.class,
        org.opengis.metadata.SpatialAttributeSupplement.class,
        org.opengis.metadata.citation.Address.class,
        org.opengis.metadata.citation.Citation.class,
        org.opengis.metadata.citation.CitationDate.class,
        org.opengis.metadata.citation.CitationFactory.class,
        org.opengis.metadata.citation.Contact.class,
        org.opengis.metadata.citation.OnLineResource.class,
        org.opengis.metadata.citation.ResponsibleParty.class,
        org.opengis.metadata.citation.Series.class,
        org.opengis.metadata.citation.Telephone.class,
        org.opengis.metadata.constraint.Constraints.class,
        org.opengis.metadata.constraint.LegalConstraints.class,
        org.opengis.metadata.constraint.SecurityConstraints.class,
        org.opengis.metadata.content.Band.class,
        org.opengis.metadata.content.ContentInformation.class,
        org.opengis.metadata.content.CoverageDescription.class,
        org.opengis.metadata.content.FeatureCatalogueDescription.class,
        org.opengis.metadata.content.ImageDescription.class,
        org.opengis.metadata.content.RangeDimension.class,
        org.opengis.metadata.distribution.DigitalTransferOptions.class,
        org.opengis.metadata.distribution.Distribution.class,
        org.opengis.metadata.distribution.Distributor.class,
        org.opengis.metadata.distribution.Format.class,
        org.opengis.metadata.distribution.Medium.class,
        org.opengis.metadata.distribution.StandardOrderProcess.class,
        org.opengis.metadata.extent.BoundingPolygon.class,
        org.opengis.metadata.extent.Extent.class,
        org.opengis.metadata.extent.GeographicBoundingBox.class,
        org.opengis.metadata.extent.GeographicDescription.class,
        org.opengis.metadata.extent.GeographicExtent.class,
        org.opengis.metadata.extent.SpatialTemporalExtent.class,
        org.opengis.metadata.extent.TemporalExtent.class,
        org.opengis.metadata.extent.VerticalExtent.class,
        org.opengis.metadata.identification.AggregateInformation.class,
        org.opengis.metadata.identification.BrowseGraphic.class,
        org.opengis.metadata.identification.DataIdentification.class,
        org.opengis.metadata.identification.Identification.class,
        org.opengis.metadata.identification.Keywords.class,
        org.opengis.metadata.identification.RepresentativeFraction.class,
        org.opengis.metadata.identification.Resolution.class,
        org.opengis.metadata.identification.ServiceIdentification.class,
        org.opengis.metadata.identification.Usage.class,
        org.opengis.metadata.lineage.Lineage.class,
        org.opengis.metadata.lineage.ProcessStep.class,
        org.opengis.metadata.lineage.Source.class,
        org.opengis.metadata.maintenance.MaintenanceInformation.class,
        org.opengis.metadata.maintenance.ScopeDescription.class,
        org.opengis.metadata.quality.AbsoluteExternalPositionalAccuracy.class,
        org.opengis.metadata.quality.AccuracyOfATimeMeasurement.class,
        org.opengis.metadata.quality.Completeness.class,
        org.opengis.metadata.quality.CompletenessCommission.class,
        org.opengis.metadata.quality.CompletenessOmission.class,
        org.opengis.metadata.quality.ConceptualConsistency.class,
        org.opengis.metadata.quality.ConformanceResult.class,
        org.opengis.metadata.quality.DataQuality.class,
        org.opengis.metadata.quality.DomainConsistency.class,
        org.opengis.metadata.quality.Element.class,
        org.opengis.metadata.quality.FormatConsistency.class,
        org.opengis.metadata.quality.GriddedDataPositionalAccuracy.class,
        org.opengis.metadata.quality.LogicalConsistency.class,
        org.opengis.metadata.quality.NonQuantitativeAttributeAccuracy.class,
        org.opengis.metadata.quality.PositionalAccuracy.class,
        org.opengis.metadata.quality.QuantitativeAttributeAccuracy.class,
        org.opengis.metadata.quality.QuantitativeResult.class,
        org.opengis.metadata.quality.RelativeInternalPositionalAccuracy.class,
        org.opengis.metadata.quality.Result.class,
        org.opengis.metadata.quality.Scope.class,
        org.opengis.metadata.quality.TemporalAccuracy.class,
        org.opengis.metadata.quality.TemporalConsistency.class,
        org.opengis.metadata.quality.TemporalValidity.class,
        org.opengis.metadata.quality.ThematicAccuracy.class,
        org.opengis.metadata.quality.ThematicClassificationCorrectness.class,
        org.opengis.metadata.quality.TopologicalConsistency.class,
        org.opengis.metadata.spatial.Dimension.class,
        org.opengis.metadata.spatial.GeometricObjects.class,
        org.opengis.metadata.spatial.Georectified.class,
        org.opengis.metadata.spatial.Georeferenceable.class,
        org.opengis.metadata.spatial.GridSpatialRepresentation.class,
        org.opengis.metadata.spatial.SpatialRepresentation.class,
        org.opengis.metadata.spatial.VectorSpatialRepresentation.class
    };

    /**
     * GeoAPI interfaces that are know to be unimplemented at this stage.
     */
    private static final Class[] UNIMPLEMENTED = new Class[] {
        AggregateInformation.class,
        CoverageContentType.class,
        ImagingCondition.class,
        CitationFactory.class,          // SHOULD THIS INTERFACE REALLY EXISTS IN GEOAPI?
        RepresentativeFraction.class,   // Implemented on top of 'Number'.
        VerticalExtent.class,           // Inconsistent 'verticalCRS' type in GeoAPI interface.
        ScopeDescription.class,         // Only partially implemented (no references to Features).
        OnLineResource.class            // No 'setProtocol' method.
    };

    /**
     * Run the suit from the command line.
     */
    public static void main(final String[] args) {
        junit.textui.TestRunner.run(suite());
    }

    /**
     * Returns the test suite.
     */
    public static Test suite() {
        return new TestSuite(ISOTest.class);
    }

    /**
     * Constructs a test case with the given name.
     */
    public ISOTest(final String name) {
        super(name);
    }

    /**
     * Ensures that the {@link #TEST} array do not contains code list.
     */
    public void testNoCodeList() {
        for (int i=0; i<TEST.length; i++) {
            final Class type = TEST[i];
            assertFalse(type.getName(), CodeList.class.isAssignableFrom(type));
        }
    }

    /**
     * Tests all dependencies starting from the {@link MetaDataImpl} class.
     */
    public void testDependencies() {
        assertNull(getImplementation(Number.class));
        assertSame(MetaDataImpl.class, getImplementation(MetaData.class));
        final Set done = new HashSet();
        for (int i=0; i<TEST.length; i++) {
            final Class type = TEST[i];
            final Class impl = getImplementation(type);
            if (impl == null) {
                if (isImplemented(type)) {
                    fail(type.getName() + " is not implemented.");
                }
                continue;
            }
            assertSetters(new PropertyAccessor(impl, type), done);
        }
        if (VERBOSE) {
            System.out.println(done);
        }
    }

    /**
     * Recursively ensures that the specified metadata implementation has
     * setters for every methods.
     */
    private static void assertSetters(final PropertyAccessor accessor, final Set/*<Class>*/ done) {
        if (done.add(accessor.type)) {
            /*
             * Tries to instantiate the implementation. Every implementation should have a
             * no-args constructor, and their instantiation should never fail. Note that
             * this dummy will also be of some help later in this test.
             */
            final Object dummyInstance;
            final boolean isImplemented = isImplemented(accessor.type);
            if (isImplemented) try {
                dummyInstance = accessor.implementation.getConstructor((Class[]) null).
                        newInstance((Object[]) null);
            } catch (Exception e) {
                fail(e.toString());
                return;
            } else {
                dummyInstance = null;
            }
            /*
             * Iterates over all properties defined in the interface,
             * and checks for the existences of a setter method.
             */
            final String classname = Utilities.getShortName(accessor.type) + '.';
            final int count = accessor.count();
            for (int i=0; i<count; i++) {
                final String name = accessor.name(i);
                assertNotNull(String.valueOf(i), name);
                final String fullname = classname + name;
                assertEquals(fullname, i, accessor.indexOf(name));
                if (!isImplemented) {
                    continue;
                }
                // We can not continue below this point for
                // implementations that are only partial.
                assertTrue(fullname, accessor.isWritable(i));
                /*
                 * Get the property type. In the special case where the property type
                 * is a collection, get an empty collection from the implementation.
                 * This is needed in order to get the element type in the collection.
                 */
                Class type = accessor.type(i);
                if (Collection.class.isAssignableFrom(type)) {
                    final Object example = accessor.get(i, dummyInstance);
                    if (example instanceof CheckedCollection) {
                        type = ((CheckedCollection) example).getElementType();
                    }
                }
                final Class impl = getImplementation(type);
                if (impl != null) {
                    assertSetters(new PropertyAccessor(impl, type), done);
                }
            }
        }
    }

    /**
     * Returns the implementation class for the specified interface class,
     * or {@code null} if none.
     */
    private static Class getImplementation(final Class type) {
        if (!CodeList.class.isAssignableFrom(type)) {
            String name = type.getName();
            if (name.startsWith(INTERFACE_PACKAGE)) {
                name = IMPLEMENTATION_PACKAGE +
                        name.substring(INTERFACE_PACKAGE.length()) + IMPLEMENTATION_SUFFIX;
                try {
                    return Class.forName(name);
                } catch (ClassNotFoundException e) {
                    /*
                     * Found a class which is not implemented. Before to report an error,
                     * check if it is part of the list of known unimplemented interfaces.
                     */
                    if (isImplemented(type)) {
                        fail(e.toString());
                    }
                }
            }
        }
        return null;
    }

    /**
     * Returns {@code true} if the specified type is not in the list of
     * known unimplemented types.
     */
    private static boolean isImplemented(final Class type) {
        for (int i=0; i<UNIMPLEMENTED.length; i++) {
            if (type.equals(UNIMPLEMENTED[i])) {
                return false;
            }
        }
        return true;
    }
}
