/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 *  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.apache.causeway.core.metamodel.facets.object.navchild;

import java.util.stream.Collectors;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.apache.causeway.applib.graph.tree.TreeNode;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.core.metamodel.context.MetaModelContext;
import org.apache.causeway.core.metamodel.facets.FacetFactoryTestAbstract;
import org.apache.causeway.core.metamodel.facets.object.navchild._TreeSample.A;
import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
import org.apache.causeway.core.mmtestsupport.MetaModelContext_forTesting;

class TreeTraversalTest
extends FacetFactoryTestAbstract {

    private MetaModelContext mmc;
    private ObjectTreeAdapter treeAdapter;
    private A a;
    private TreeNode<Object> tree;

    @BeforeEach
    void setUp() {
        mmc = MetaModelContext_forTesting.builder()
                .enablePostprocessors(true)
                .build();
        treeAdapter = new ObjectTreeAdapter(mmc.getSpecificationLoader());
        // sample tree, that we then traverse
        a = _TreeSample.sampleA();
        tree = TreeNode.root(a, treeAdapter);

        //TODO[causeway-core-metamodel-CAUSEWAY-2297] ObjectSpecification#streamAssociations seems to have the side effect
        // of initializing the various members with the NavigableSubtreeSequenceFacet,
        // which otherwise does not happen (is this test specific? if so then good, but we should fix that)
        var specs = Can.of(_TreeSample.A.class, _TreeSample.B.class, _TreeSample.C.class, _TreeSample.D.class)
            .map(mmc.getSpecificationLoader()::specForType)
            .map(opt->opt.orElse(null));
        specs.forEach(spec->spec.streamAssociations(MixedIn.EXCLUDED).forEach(assoc->{}));
    }

    @Test
    void preconditions() {
        var specLoader = mmc.getSpecificationLoader();
        var specA = specLoader.loadSpecification(_TreeSample.A.class);
        // including this one to test presence of NavigableSubtreeFacet w/o the side-effect of calling the getAssociation method
        var specB = specLoader.loadSpecification(_TreeSample.B.class);

        // first: members must have the NavigableSubtreeSequenceFacet
        var assocAB = specA.getAssociationElseFail("childrenB");
        assertTrue(assocAB.isCollection());
        assertTrue(assocAB.containsFacet(NavigableSubtreeSequenceFacet.class));

        // java map support
        var assocAC = specA.getAssociationElseFail("childrenC");
        assertTrue(assocAC.isCollection());
        assertTrue(assocAC.containsFacet(NavigableSubtreeSequenceFacet.class));
        assertEquals(_TreeSample.C.class, assocAC.getElementType().getCorrespondingClass());

        // second: post-processor should generate NavigableSubtreeFacet
        assertTrue(specA.containsFacet(NavigableSubtreeFacet.class));
        assertTrue(specB.containsFacet(NavigableSubtreeFacet.class));

        // tree sanity checks
        assertEquals(a, tree.value());
        assertNotNull(tree.rootNode());
        assertEquals(tree, tree.rootNode());
        assertTrue(tree.isRoot());
        assertFalse(tree.isLeaf());

        // node a is expected to have 4 children
        var navigableSubtreeFacet = specA.getFacet(NavigableSubtreeFacet.class);
        assertEquals(4, navigableSubtreeFacet.childCountOf(a));
        assertEquals(4, navigableSubtreeFacet.childrenOf(a).toList().size());
        assertEquals(4, treeAdapter.childCountOf(a));
        assertEquals(4, treeAdapter.childrenOf(a).toList().size());
        assertEquals(4, tree.childCount());
        assertEquals(4, tree.streamChildren().toList().size());

        var firstChildOfA = treeAdapter.childrenOf(a).findFirst().orElseThrow();

        // first child node of a is expected to have 4 children
        assertEquals(4, treeAdapter.childCountOf(firstChildOfA));
        assertEquals(4, treeAdapter.childrenOf(firstChildOfA).toList().size());

        // count all nodes
        assertEquals(25, Can.ofIterable(tree::iteratorDepthFirst).size());
        assertEquals(25, Can.ofIterable(tree::iteratorBreadthFirst).size());
    }

    @Test
    void depthFirstTraversal() {
        var nodeNames = tree.streamDepthFirst()
            .map(TreeNode::value)
            .map(_TreeSample::nameOf)
            .collect(Collectors.joining(", "));

        assertEquals(
                "a, b1, c1, d1, d2, d3, d1, d2, d3, b2, c2, d1, d2, d3, d1, d2, d3, c1, d1, d2, d3, c2, d1, d2, d3",
                nodeNames);
    }

    @Test
    void leafToRootTraversal() {
        // pick an arbitrary leaf value,
        // from which we then traverse up to the root
        var d = a.childrenB().getFirstElseFail().childrenD().getLastElseFail();

        // find d's node
        var leafNode = tree.streamDepthFirst()
                .filter((final TreeNode<Object> treeNode)->d.equals(treeNode.value()))
                .findFirst()
                .orElseThrow();

        var nodeNames = leafNode.streamHierarchyUp()
            .map(TreeNode::value)
            .map(_TreeSample::nameOf)
            .collect(Collectors.joining(", "));

        assertEquals(
                "d3, b1, a",
                nodeNames);
    }

}

