/*
 * 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.openjpa.persistence.kernel;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;

import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.OracleDictionary;
import org.apache.openjpa.persistence.FetchPlan;
import org.apache.openjpa.persistence.OpenJPAPersistence;
import org.apache.openjpa.persistence.kernel.common.apps.State;
import org.apache.openjpa.persistence.kernel.common.apps.Transition;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;

/**
 * Test complex relationship graph fetch.
 * The graph is represented by State (node) and Transition(edges).
 * Graph nodes (State) holds pair of list of incoming/outgoing edges (Transition).
 * The problem report [1] showed that traversal from a root node (incorrectly) terminates
 * at a depth of 1 irrespective of the value of recursion depth and/or max depth depth.
 *
 * The test data model is originally reported in
 * [1] FetchGroup Recursion Problem
 * <A HREF="http://n2.nabble.com/Fetchgroups-recursion-problem-tc3874382.html#a3874382">mailing list</A>.
 *
 *
 * @author Pinaki Poddar
 *
 */
public class TestIndirectRecursion extends SingleEMFTestCase {
    // The connection matrix
    static byte[][] transitions = {
      //  s1 s2 s3 s4 s5
        { 0, 1, 0, 0, 0 }, // s1
        { 1, 0, 1, 1, 0 }, // s2
        { 1, 1, 0, 1, 0 }, // s3
        { 0, 0, 0, 0, 1 }, // s4
        { 0, 0, 0, 0, 0 }  // s5
    };

    @Override
    public void setUp() {
        super.setUp(DROP_TABLES, State.class, Transition.class);

        DBDictionary dict = ((JDBCConfiguration) emf.getConfiguration()).getDBDictionaryInstance();
        if (dict instanceof OracleDictionary) {
            ((OracleDictionary) dict).useTriggersForAutoAssign = true;
        }
    }

    public void testFetch() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();

        int N = transitions.length;
        State[] states = new State[N];
        // create nodes
        for (int i = 1; i <= N; i++) {
            State s = new State();
            s.setName("s" + i);
            em.persist(s);
            states[i - 1] = s;
        }
        // create edges as per the transition matrix
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                if (transitions[i][j] == 1) {
                    newTransition(states[i], states[j]);
                }
            }
        }
        em.getTransaction().commit();

        em.clear();

        // Select root (state 1)
        Query query = em.createQuery("select s from State s where s.name=:name");
        FetchPlan fetch = OpenJPAPersistence.cast(query).getFetchPlan();
        fetch.setMaxFetchDepth(15);
        fetch.addField(State.class, "incomingTransitions");
        fetch.addField(State.class, "outgoingTransitions");
        fetch.addField(Transition.class, "toState");
        fetch.addField(Transition.class, "fromState");
        State qs1 = (State) query.setParameter("name", "s1").getSingleResult();

        em.close(); // will not load anything anymore

        byte[][] actualTransitionMatrix = new byte[5][5];
        fillTransitionMatrix(actualTransitionMatrix, new HashSet<>(), qs1);
        assertMatrixEqual(transitions, actualTransitionMatrix);

    }

    void assertMatrixEqual(byte[][] expected, byte[][] actual) {
        int N = transitions.length;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                assertEquals("Transition(s" + (i+1) + ", s" + (j+1) + ") does not match", expected[i][j], actual[i][j]);
            }
        }
    }

    void fillTransitionMatrix(byte[][] matrix, Set<State> visited, State s) {
        if (visited.contains(s))
            return;
        List<Transition> outgoings = s.getOutgoingTransitions();
        if (outgoings != null) {
            for (Transition t : outgoings) {
                fillTransitionMatrix(matrix, t);
            }
        }
        visited.add(s);

        if (outgoings != null) {
            for (Transition t : outgoings) {
                fillTransitionMatrix(matrix, visited, t.getToState());
            }
        }

    }

    void fillTransitionMatrix(byte[][] matrix, Transition t) {
        matrix[getIndex(t.getFromState())][getIndex(t.getToState())] = 1;
    }

    int getIndex(State s) {
        return Integer.parseInt(s.getName().substring(1)) - 1;
    }

    Transition newTransition(State from, State to) {
        Transition t = new Transition();
        t.setFromState(from);
        t.setToState(to);
        t.setName(from.getName()+"->"+to.getName());
        from.addOutgoingTransitions(t);
        to.addIncomingTransitions(t);
        return t;
    }

    /**
     * Find a state of the given name in the list of outgoing transition of the
     * given State.
     */
    State findOutgoingState(String name, State root) {
        List<Transition> transitions = root.getOutgoingTransitions();
        for (Transition t : transitions) {
            if (t.getToState().getName().equals(name))
                return t.getToState();
        }
        return null;
    }

    /**
     * Find a state of the given name in the list of incoming transition of the
     * given State.
     */
    State findIncomingState(String name, State root) {
        List<Transition> transitions = root.getIncomingTransitions();
        for (Transition t : transitions) {
            if (t.getFromState().getName().equals(name))
                return t.getFromState();
        }
        return null;
    }

    /**
     * Asserts the the origin state has the given incoming states.
     */
    void assertIncomingStates(State origin, State... expected) {
        assertNotNull(origin);
        for (State e : expected) {
            assertNotNull(e + " in not an incoimng states of " + origin, findIncomingState(e.getName(), origin));
        }
    }

    /**
     * Asserts the the origin state has the given outgoing states.
     */
    void assertOutgoingStates(State origin, State... expected) {
        assertNotNull(origin);
        for (State e : expected) {
            assertNotNull(e + " in not an incoimng states of " + origin, findOutgoingState(e.getName(), origin));
        }
    }
}
