/*
 *  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.mina.statemachine;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.util.LinkedList;

import org.apache.mina.statemachine.annotation.Transition;
import org.apache.mina.statemachine.annotation.Transitions;
import org.apache.mina.statemachine.annotation.OnEntry;
import org.apache.mina.statemachine.annotation.OnExit;
import org.apache.mina.statemachine.context.StateContext;
import org.apache.mina.statemachine.event.Event;
import org.apache.mina.statemachine.transition.MethodSelfTransition;
import org.apache.mina.statemachine.transition.MethodTransition;
import org.junit.Test;

/**
 * Tests {@link StateMachineProxyBuilder}.
 *
 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
 */
public class StateMachineProxyBuilderTest {
    @Test
    public void testReentrantStateMachine() throws Exception {
        ReentrantStateMachineHandler handler = new ReentrantStateMachineHandler();

        State s1 = new State("s1");
        State s2 = new State("s2");
        State s3 = new State("s3");

        s1.addTransition(new MethodTransition("call1", s2, handler));
        s2.addTransition(new MethodTransition("call2", s3, handler));
        s3.addTransition(new MethodTransition("call3", handler));

        StateMachine sm = new StateMachine(new State[] { s1, s2, s3 }, "s1");
        Reentrant reentrant = new StateMachineProxyBuilder().create(Reentrant.class, sm);
        reentrant.call1(reentrant);
        assertTrue(handler.finished);
    }

    @Test
    public void testTapeDeckStateMachine() throws Exception {
        TapeDeckStateMachineHandler handler = new TapeDeckStateMachineHandler();

        State parent = new State("parent");
        State s1 = new State("s1", parent);
        State s2 = new State("s2", parent);
        State s3 = new State("s3", parent);
        State s4 = new State("s4", parent);
        State s5 = new State("s5", parent);

        parent.addTransition(new MethodTransition("*", "error", handler));
        s1.addTransition(new MethodTransition("insert", s2, "inserted", handler));
        s2.addTransition(new MethodTransition("start", s3, "playing", handler));
        s3.addTransition(new MethodTransition("stop", s4, "stopped", handler));
        s3.addTransition(new MethodTransition("pause", s5, "paused", handler));
        s4.addTransition(new MethodTransition("eject", s1, "ejected", handler));
        s5.addTransition(new MethodTransition("pause", s3, "playing", handler));

        s2.addOnEntrySelfTransaction(new MethodSelfTransition("onEntryS2", handler));
        s2.addOnExitSelfTransaction(new MethodSelfTransition("onExitS2", handler));

        s3.addOnEntrySelfTransaction(new MethodSelfTransition("onEntryS3", handler));
        s3.addOnExitSelfTransaction(new MethodSelfTransition("onExitS3", handler));

        s4.addOnEntrySelfTransaction(new MethodSelfTransition("onEntryS4", handler));
        s4.addOnExitSelfTransaction(new MethodSelfTransition("onExitS4", handler));

        StateMachine sm = new StateMachine(new State[] { s1, s2, s3, s4, s5 }, "s1");
        TapeDeck player = new StateMachineProxyBuilder().create(TapeDeck.class, sm);
        player.insert("Kings of convenience - Riot on an empty street");
        player.start();
        player.pause();
        player.pause();
        player.eject();
        player.stop();
        player.eject();

        LinkedList<String> messages = handler.messages;
        assertEquals("Tape 'Kings of convenience - Riot on an empty street' inserted", messages.removeFirst());
        assertEquals("S2 entered", messages.removeFirst());
        assertEquals("Playing", messages.removeFirst());
        assertEquals("S2 exited", messages.removeFirst());
        assertEquals("S3 entered with stateContext", messages.removeFirst());
        assertEquals("Paused", messages.removeFirst());
        assertEquals("S3 exited with stateContext", messages.removeFirst());
        assertEquals("Playing", messages.removeFirst());
        assertEquals("S3 entered with stateContext", messages.removeFirst());
        assertEquals("Error: Cannot eject at this time", messages.removeFirst());
        assertEquals("Stopped", messages.removeFirst());
        assertEquals("S3 exited with stateContext", messages.removeFirst());
        assertEquals("S4 entered with stateContext and state", messages.removeFirst());
        assertEquals("Tape ejected", messages.removeFirst());
        assertEquals("S4 exited with stateContext and state", messages.removeFirst());

        assertTrue(messages.isEmpty());
    }

    @Test
    public void testTapeDeckStateMachineAnnotations() throws Exception {
        TapeDeckStateMachineHandler handler = new TapeDeckStateMachineHandler();

        StateMachine sm = StateMachineFactory.getInstance(Transition.class).create(TapeDeckStateMachineHandler.S1,
                handler);

        TapeDeck player = new StateMachineProxyBuilder().create(TapeDeck.class, sm);
        player.insert("Kings of convenience - Riot on an empty street");
        player.start();
        player.pause();
        player.pause();
        player.eject();
        player.stop();
        player.eject();

        LinkedList<String> messages = handler.messages;
        assertEquals("Tape 'Kings of convenience - Riot on an empty street' inserted", messages.removeFirst());
        assertEquals("S2 entered", messages.removeFirst());
        assertEquals("Playing", messages.removeFirst());
        assertEquals("S2 exited", messages.removeFirst());
        assertEquals("S3 entered with stateContext", messages.removeFirst());
        assertEquals("Paused", messages.removeFirst());
        assertEquals("S3 exited with stateContext", messages.removeFirst());
        assertEquals("Playing", messages.removeFirst());
        assertEquals("S3 entered with stateContext", messages.removeFirst());
        assertEquals("Error: Cannot eject at this time", messages.removeFirst());
        assertEquals("Stopped", messages.removeFirst());
        assertEquals("S3 exited with stateContext", messages.removeFirst());
        assertEquals("S4 entered with stateContext and state", messages.removeFirst());
        assertEquals("Tape ejected", messages.removeFirst());
        assertEquals("S4 exited with stateContext and state", messages.removeFirst());

        assertTrue(messages.isEmpty());
    }

    public interface Reentrant {
        void call1(Reentrant proxy);

        void call2(Reentrant proxy);

        void call3(Reentrant proxy);
    }

    public static class ReentrantStateMachineHandler {
        private boolean finished = false;

        public void call1(Reentrant proxy) {
            proxy.call2(proxy);
        }

        public void call2(Reentrant proxy) {
            proxy.call3(proxy);
        }

        public void call3(Reentrant proxy) {
            finished = true;
        }
    }

    public interface TapeDeck {
        void insert(String name);

        void eject();

        void start();

        void pause();

        void stop();
    }

    public static class TapeDeckStateMachineHandler {
        @org.apache.mina.statemachine.annotation.State
        public static final String PARENT = "parent";

        @org.apache.mina.statemachine.annotation.State(PARENT)
        public static final String S1 = "s1";

        @org.apache.mina.statemachine.annotation.State(PARENT)
        public static final String S2 = "s2";

        @org.apache.mina.statemachine.annotation.State(PARENT)
        public static final String S3 = "s3";

        @org.apache.mina.statemachine.annotation.State(PARENT)
        public static final String S4 = "s4";

        @org.apache.mina.statemachine.annotation.State(PARENT)
        public static final String S5 = "s5";

        private LinkedList<String> messages = new LinkedList<>();

        @OnEntry(S2)
        public void onEntryS2() {
            messages.add("S2 entered");
        }

        @OnExit(S2)
        public void onExitS2() {
            messages.add("S2 exited");
        }

        @OnEntry(S3)
        public void onEntryS3(StateContext stateContext) {
            messages.add("S3 entered with stateContext");
        }

        @OnExit(S3)
        public void onExitS3(StateContext stateContext) {
            messages.add("S3 exited with stateContext");
        }

        @OnEntry(S4)
        public void onEntryS4(StateContext stateContext, State state) {
            messages.add("S4 entered with stateContext and state");
        }

        @OnExit(S4)
        public void onExitS4(StateContext stateContext, State state) {
            messages.add("S4 exited with stateContext and state");
        }

        @Transition(on = "insert", in = "s1", next = "s2")
        public void inserted(String name) {
            messages.add("Tape '" + name + "' inserted");
        }

        @Transition(on = "eject", in = "s4", next = "s1")
        public void ejected() {
            messages.add("Tape ejected");
        }

        @Transitions({ @Transition(on = "start", in = "s2", next = "s3"),
                @Transition(on = "pause", in = "s5", next = "s3") })
        public void playing() {
            messages.add("Playing");
        }

        @Transition(on = "pause", in = "s3", next = "s5")
        public void paused() {
            messages.add("Paused");
        }

        @Transition(on = "stop", in = "s3", next = "s4")
        public void stopped() {
            messages.add("Stopped");
        }

        @Transition(on = "*", in = "parent")
        public void error(Event event) {
            messages.add("Error: Cannot " + event.getId() + " at this time");
        }
    }
}
