/*
 * Copyright 2010 the original author or authors.
 *
 * Licensed 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.gradle.messaging.actor.internal

import org.gradle.messaging.actor.Actor
import org.gradle.messaging.dispatch.DispatchException
import org.gradle.messaging.dispatch.MethodInvocation
import org.gradle.util.JUnit4GroovyMockery
import org.gradle.util.MultithreadedTestCase
import org.jmock.integration.junit4.JMock
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import org.gradle.internal.concurrent.ThreadSafe

@RunWith(JMock.class)
class DefaultActorFactoryTest extends MultithreadedTestCase {
    private final JUnit4GroovyMockery context = new JUnit4GroovyMockery()
    private final TargetObject target = context.mock(TargetObject.class)
    private final DefaultActorFactory factory = new DefaultActorFactory(executorFactory)

    @After
    public void tearDown() {
        factory.stop()
    }

    @Test
    public void createsANonBlockingActorForATargetObject() {
        def actor = factory.createActor(target)
        assertThat(actor, notNullValue())
    }

    @Test
    public void cachesTheNonBlockingActorForATargetObject() {
        Actor actor1 = factory.createActor(target)
        Actor actor2 = factory.createActor(target)
        assertThat(actor2, sameInstance(actor1))
    }

    @Test
    public void returnsTargetObjectIfTargetObjectIsAnActor() {
        Actor actor1 = factory.createActor(target)
        Actor actor2 = factory.createActor(actor1)
        assertThat(actor2, sameInstance(actor1))
    }

    @Test
    public void nonBlockingActorAndProxyAreBothMarkedAsThreadSafe() {
        def actor = factory.createActor(target)
        assertThat(actor, instanceOf(ThreadSafe))
        assertThat(actor.getProxy(Runnable), instanceOf(ThreadSafe))
    }

    @Test
    public void createsABlockingActorForATargetObject() {
        def actor = factory.createBlockingActor(target)
        assertThat(actor, notNullValue())
    }

    @Test
    public void cachesTheBlockingActorForATargetObject() {
        Actor actor1 = factory.createBlockingActor(target)
        Actor actor2 = factory.createBlockingActor(target)
        assertThat(actor2, sameInstance(actor1))
    }

    @Test
    public void blockingActorAndProxyAreBothMarkedAsThreadSafe() {
        def actor = factory.createBlockingActor(target)
        assertThat(actor, instanceOf(ThreadSafe))
        assertThat(actor.getProxy(Runnable), instanceOf(ThreadSafe))
    }

    @Test
    public void blockingActorDispatchesMethodInvocationToTargetObjectAndWaitsForResult() {
        Actor actor = factory.createBlockingActor(target)

        context.checking {
            one(target).doStuff('param')
        }

        actor.dispatch(new MethodInvocation(TargetObject.class.getMethod('doStuff', String.class), ['param'] as Object[]))
    }

    @Test
    public void blockingActorProxyDispatchesMethodInvocationToTargetObjectAndWaitsForResult() {
        Actor actor = factory.createBlockingActor(target)

        context.checking {
            one(target).doStuff('param')
        }

        actor.getProxy(TargetObject).doStuff('param')
    }

    @Test
    public void blockingActorDispatchesMethodInvocationFromOneThreadAtATime() {
        Actor actor = factory.createBlockingActor(target)

        context.checking {
            one(target).doStuff('param')
            will {
                syncAt(1)
            }
            one(target).doStuff('param2')
        }
        
        def proxy = actor.getProxy(TargetObject)
        start {
            proxy.doStuff('param')
        }
        run {
            expectBlocksUntil(1) {
                proxy.doStuff('param2')
            }
        }
    }

    @Test
    public void nonBlockingActorDispatchesMethodInvocationToTargetObjectAndDoesNotWaitForResult() {
        Actor actor = factory.createActor(target)

        context.checking {
            one(target).doStuff('param')
            will {
                syncAt(1)
            }
        }

        run {
            actor.dispatch(new MethodInvocation(TargetObject.class.getMethod('doStuff', String.class), ['param'] as Object[]))
            syncAt(1)
        }
    }

    @Test
    public void nonBlockingActorProxyDispatchesMethodCallToTargetObjectAndDoesNotWaitForResult() {
        Actor actor = factory.createActor(target)
        TargetObject proxy = actor.getProxy(TargetObject.class)

        context.checking {
            one(target).doStuff('param')
            will {
                syncAt(1)
            }
        }

        run {
            proxy.doStuff('param')
            syncAt(1)
        }
    }

    @Test
    public void nonBlockingActorPropagatesMethodFailuresOnStop() {
        Actor actor = factory.createActor(target)
        TargetObject proxy = actor.getProxy(TargetObject.class)
        RuntimeException failure = new RuntimeException()

        context.checking {
            one(target).doStuff('param')
            will {
                throw failure
            }
        }

        run {
            proxy.doStuff('param')
            try {
                actor.stop()
                fail()
            } catch (DispatchException e) {
                assertThat(e.message, startsWith("Could not dispatch message"))
                assertThat(e.cause, sameInstance(failure))
            }
        }
    }

    @Test
    public void nonBlockingActorStopBlocksUntilAllMethodCallsComplete() {
        Actor actor = factory.createActor(target)
        TargetObject proxy = actor.getProxy(TargetObject.class)

        context.checking {
            one(target).doStuff('param')
            will {
                syncAt(1)
            }
        }

        run {
            proxy.doStuff('param')
            expectBlocksUntil(1) {
                actor.stop()
            }
        }
    }

    @Test
    public void blockingActorStopBlocksUntilAllMethodCallsComplete() {
        Actor actor = factory.createBlockingActor(target)
        TargetObject proxy = actor.getProxy(TargetObject.class)

        context.checking {
            one(target).doStuff('param')
            will {
                syncAt(1)
            }
        }

        start {
            proxy.doStuff('param')
        }
        run {
            expectBlocksUntil(1) {
                actor.stop()
            }
        }
    }

    @Test
    public void factoryStopBlocksUntilAllMethodCallsComplete() {
        Actor actor = factory.createActor(target)
        TargetObject proxy = actor.getProxy(TargetObject.class)

        context.checking {
            one(target).doStuff('param')
            will {
                syncAt(1)
            }
        }

        run {
            proxy.doStuff('param')
            expectBlocksUntil(1) {
                factory.stop()
            }
        }
    }
    
    @Test
    public void cannotDispatchToBlockingActorAfterStopped() {
        Actor actor = factory.createBlockingActor(target)
        TargetObject proxy = actor.getProxy(TargetObject.class)

        actor.stop()
        try {
            proxy.doStuff('param')
            fail()
        } catch (IllegalStateException e) {
            // Expected
        }
    }

    @Test
    public void cannotDispatchToNonBlockingActorAfterStopped() {
        Actor actor = factory.createActor(target)
        TargetObject proxy = actor.getProxy(TargetObject.class)

        actor.stop()
        try {
            proxy.doStuff('param')
            fail()
        } catch (IllegalStateException e) {
            // Expected
        }
    }
}

interface TargetObject {
    void doStuff(String param)

    void stopDoingStuff()
}
