/*
 * 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.jmeter.control;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

import org.apache.jmeter.engine.util.ValueReplacer;
import org.apache.jmeter.junit.JMeterTestCase;
import org.apache.jmeter.junit.stubs.TestSampler;
import org.apache.jmeter.samplers.Sampler;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.apache.jmeter.threads.JMeterContext;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterThread;
import org.apache.jmeter.threads.JMeterVariables;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class TestWhileController extends JMeterTestCase {

    private JMeterContext jmctx;
    private JMeterVariables jmvars;

    @BeforeEach
    public void setUp() {
        jmctx = JMeterContextService.getContext();
        jmctx.setVariables(new JMeterVariables());
        jmvars = jmctx.getVariables();
    }

    private void setLastSampleStatus(boolean status) {
        jmvars.put(JMeterThread.LAST_SAMPLE_OK, Boolean.toString(status));
    }

    private void setRunning(TestElement el) {
        PropertyIterator pi = el.propertyIterator();
        while (pi.hasNext()) {
            pi.next().setRunningVersion(true);
        }
    }

    // Get next sample and its name
    private String nextName(GenericController c) {
        Sampler s = c.next();
        if (s == null) {
            return null;
        }
        return s.getName();
    }

    // While (blank), previous sample OK - should loop until false
    @Test
    public void testBlankPrevOK() throws Exception {
        runtestPrevOK("");
    }

    // While (LAST), previous sample OK - should loop until false
    @Test
    public void testLastPrevOK() throws Exception {
        runtestPrevOK("LAST");
    }

    private static final String OTHER = "X"; // Dummy for testing functions

    // While (LAST), previous sample OK - should loop until false
    @Test
    public void testOtherPrevOK() throws Exception {
        runtestPrevOK(OTHER);
    }

    private void runtestPrevOK(String type) throws Exception {
        GenericController controller = new GenericController();
        WhileController while_cont = new WhileController();
        setLastSampleStatus(true);
        while_cont.setCondition(type);
        while_cont.addTestElement(new TestSampler("one"));
        while_cont.addTestElement(new TestSampler("two"));
        while_cont.addTestElement(new TestSampler("three"));
        controller.addTestElement(while_cont);
        controller.addTestElement(new TestSampler("four"));
        controller.initialize();
        assertEquals("one", nextName(controller));
        assertEquals("two", nextName(controller));
        assertEquals("three", nextName(controller));
        assertEquals("one", nextName(controller));
        assertEquals("two", nextName(controller));
        assertEquals("three", nextName(controller));
        assertEquals("one", nextName(controller));
        setLastSampleStatus(false);
        if (type.equals(OTHER)) {
            while_cont.setCondition("false");
        }
        assertEquals("two", nextName(controller));
        assertEquals("three", nextName(controller));
        setLastSampleStatus(true);
        if (type.equals(OTHER)) {
            while_cont.setCondition(OTHER);
        }
        assertEquals("one", nextName(controller));
        assertEquals("two", nextName(controller));
        assertEquals("three", nextName(controller));
        setLastSampleStatus(false);
        if (type.equals(OTHER)) {
            while_cont.setCondition("false");
        }
        assertEquals("four", nextName(controller));
        assertNull(nextName(controller));
        setLastSampleStatus(true);
        if (type.equals(OTHER)) {
            while_cont.setCondition(OTHER);
        }
        assertEquals("one", nextName(controller));
    }

    // While (blank), previous sample failed - should run once
    @Test
    public void testBlankPrevFailed() throws Exception {
        GenericController controller = new GenericController();
        controller.setRunningVersion(true);
        WhileController while_cont = new WhileController();
        setLastSampleStatus(false);
        while_cont.setCondition("");
        while_cont.addTestElement(new TestSampler("one"));
        while_cont.addTestElement(new TestSampler("two"));
        controller.addTestElement(while_cont);
        controller.addTestElement(new TestSampler("three"));
        controller.initialize();
        assertEquals("one", nextName(controller));
        assertEquals("two", nextName(controller));
        assertEquals("three", nextName(controller));
        assertNull(nextName(controller));
        // Run entire test again
        assertEquals("one", nextName(controller));
        assertEquals("two", nextName(controller));
        assertEquals("three", nextName(controller));
        assertNull(nextName(controller));
    }

    /*
     * Generic Controller
     * - before
     * - While Controller ${VAR}
     * - - one
     * - - two
     * - - Simple Controller
     * - - - three
     * - - - four
     * - after
     */
    @Test
    public void testVariable1() throws Exception {
        GenericController controller = new GenericController();
        WhileController while_cont = new WhileController();
        setLastSampleStatus(false);
        while_cont.setCondition("${VAR}");
        jmvars.put("VAR", "");
        ValueReplacer vr = new ValueReplacer();
        vr.replaceValues(while_cont);
        setRunning(while_cont);
        controller.addTestElement(new TestSampler("before"));
        controller.addTestElement(while_cont);
        while_cont.addTestElement(new TestSampler("one"));
        while_cont.addTestElement(new TestSampler("two"));
        GenericController simple = new GenericController();
        while_cont.addTestElement(simple);
        simple.addTestElement(new TestSampler("three"));
        simple.addTestElement(new TestSampler("four"));
        controller.addTestElement(new TestSampler("after"));
        controller.initialize();
        for (int i = 1; i <= 3; i++) {
            assertEquals("Loop: " + i, "before", nextName(controller));
            assertEquals("Loop: " + i, "one", nextName(controller));
            assertEquals("Loop: " + i, "two", nextName(controller));
            assertEquals("Loop: " + i, "three", nextName(controller));
            assertEquals("Loop: " + i, "four", nextName(controller));
            assertEquals("Loop: " + i, "after", nextName(controller));
            assertNull("Loop: " + i, nextName(controller));
        }
        jmvars.put("VAR", "LAST"); // Should not enter the loop
        for (int i = 1; i <= 3; i++) {
            assertEquals("Loop: " + i, "before", nextName(controller));
            assertEquals("Loop: " + i, "after", nextName(controller));
            assertNull("Loop: " + i, nextName(controller));
        }
        jmvars.put("VAR", "");
        for (int i = 1; i <= 3; i++) {
            assertEquals("Loop: " + i, "before", nextName(controller));
            if (i == 1) {
                assertEquals("Loop: " + i, "one", nextName(controller));
                assertEquals("Loop: " + i, "two", nextName(controller));
                assertEquals("Loop: " + i, "three", nextName(controller));
                jmvars.put("VAR", "LAST"); // Should not enter the loop next time
                assertEquals("Loop: " + i, "four", nextName(controller));
            }
            assertEquals("Loop: " + i, "after", nextName(controller));
            assertNull("Loop: " + i, nextName(controller));
        }
    }

    // Test with SimpleController as first item
    @Test
    public void testVariable2() throws Exception {
        GenericController controller = new GenericController();
        WhileController while_cont = new WhileController();
        setLastSampleStatus(false);
        while_cont.setCondition("${VAR}");
        jmvars.put("VAR", "");
        ValueReplacer vr = new ValueReplacer();
        vr.replaceValues(while_cont);
        setRunning(while_cont);
        controller.addTestElement(new TestSampler("before"));
        controller.addTestElement(while_cont);
        GenericController simple = new GenericController();
        while_cont.addTestElement(simple);
        simple.addTestElement(new TestSampler("one"));
        simple.addTestElement(new TestSampler("two"));
        while_cont.addTestElement(new TestSampler("three"));
        while_cont.addTestElement(new TestSampler("four"));
        controller.addTestElement(new TestSampler("after"));
        controller.initialize();
        for (int i = 1; i <= 3; i++) {
            assertEquals("Loop: " + i, "before", nextName(controller));
            assertEquals("Loop: " + i, "one", nextName(controller));
            assertEquals("Loop: " + i, "two", nextName(controller));
            assertEquals("Loop: " + i, "three", nextName(controller));
            assertEquals("Loop: " + i, "four", nextName(controller));
            assertEquals("Loop: " + i, "after", nextName(controller));
            assertNull("Loop: " + i, nextName(controller));
        }
        jmvars.put("VAR", "LAST"); // Should not enter the loop
        for (int i = 1; i <= 3; i++) {
            assertEquals("Loop: " + i, "before", nextName(controller));
            assertEquals("Loop: " + i, "after", nextName(controller));
            assertNull("Loop: " + i, nextName(controller));
        }
        jmvars.put("VAR", "");
        for (int i = 1; i <= 3; i++) {
            assertEquals("Loop: " + i, "before", nextName(controller));
            if (i == 1) {
                assertEquals("Loop: " + i, "one", nextName(controller));
                assertEquals("Loop: " + i, "two", nextName(controller));
                jmvars.put("VAR", "LAST"); // Should not enter the loop next time
                // But should continue to the end of the loop
                assertEquals("Loop: " + i, "three", nextName(controller));
                assertEquals("Loop: " + i, "four", nextName(controller));
            }
            assertEquals("Loop: " + i, "after", nextName(controller));
            assertNull("Loop: " + i, nextName(controller));
        }
    }

    // While LAST, previous sample failed - should not run
    @Test
    public void testLASTPrevFailed() throws Exception {
        runTestPrevFailed("LAST");
    }

    // While False, previous sample failed - should not run
    @Test
    public void testfalsePrevFailed() throws Exception {
        runTestPrevFailed("False");
    }

    private void runTestPrevFailed(String s) throws Exception {
        GenericController controller = new GenericController();
        WhileController while_cont = new WhileController();
        setLastSampleStatus(false);
        while_cont.setCondition(s);
        while_cont.addTestElement(new TestSampler("one"));
        while_cont.addTestElement(new TestSampler("two"));
        controller.addTestElement(while_cont);
        controller.addTestElement(new TestSampler("three"));
        controller.initialize();
        assertEquals("three", nextName(controller));
        assertNull(nextName(controller));
        assertEquals("three", nextName(controller));
        assertNull(nextName(controller));
    }

    @Test
    public void testLastFailedBlank() throws Exception {
        runTestLastFailed("");
    }

    @Test
    public void testLastFailedLast() throws Exception {
        runTestLastFailed("LAST");
    }

    // Should behave the same for blank and LAST because success on input
    private void runTestLastFailed(String s) throws Exception {
        GenericController controller = new GenericController();
        controller.addTestElement(new TestSampler("1"));
        WhileController while_cont = new WhileController();
        controller.addTestElement(while_cont);
        while_cont.setCondition(s);
        GenericController sub = new GenericController();
        while_cont.addTestElement(sub);
        sub.addTestElement(new TestSampler("2"));
        sub.addTestElement(new TestSampler("3"));

        controller.addTestElement(new TestSampler("4"));

        setLastSampleStatus(true);
        controller.initialize();
        assertEquals("1", nextName(controller));
        assertEquals("2", nextName(controller));
        setLastSampleStatus(false);
        assertEquals("3", nextName(controller));
        assertEquals("4", nextName(controller));
        assertNull(nextName(controller));
    }

    // Tests for Stack Overflow (bug 33954)
    @Test
    public void testAlwaysFailOK() throws Exception {
        runTestAlwaysFail(true); // Should be OK
    }

    @Test
    public void testAlwaysFailBAD() throws Exception {
        runTestAlwaysFail(false);
    }

    private void runTestAlwaysFail(boolean other) {
        LoopController controller = new LoopController();
        controller.setContinueForever(true);
        controller.setLoops(-1);
        WhileController while_cont = new WhileController();
        setLastSampleStatus(false);
        while_cont.setCondition("false");
        while_cont.addTestElement(new TestSampler("one"));
        while_cont.addTestElement(new TestSampler("two"));
        controller.addTestElement(while_cont);
        if (other) {
            controller.addTestElement(new TestSampler("three"));
        }
        controller.initialize();
        try {
            if (other) {
                assertEquals("three", nextName(controller));
            } else {
                assertNull(nextName(controller));
            }
        } catch (StackOverflowError e) {
            fail(e.toString());
        }
    }
}
