/**
 * ***************************************************************************
 * Copyright (c) 2010 Qcadoo Limited
 * Project: Qcadoo MES
 * Version: 1.4
 *
 * This file is part of Qcadoo.
 *
 * Qcadoo is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation; either version 3 of the License,
 * or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 * ***************************************************************************
 */
package com.qcadoo.mes.states.aop;

import static org.junit.Assert.assertEquals;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.*;

import java.util.Collections;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import com.google.common.collect.Lists;
import com.qcadoo.mes.states.MockStateChangeDescriber;
import com.qcadoo.mes.states.StateChangeContext;
import com.qcadoo.mes.states.StateChangeEntityDescriber;
import com.qcadoo.mes.states.StateChangeTest;
import com.qcadoo.mes.states.annotation.RunInPhase;
import com.qcadoo.mes.states.constants.StateChangeStatus;
import com.qcadoo.mes.states.exception.StateChangeException;
import com.qcadoo.mes.states.messages.constants.StateMessageType;
import com.qcadoo.model.api.DataDefinition;
import com.qcadoo.model.api.Entity;
import com.qcadoo.model.api.EntityList;

public class AbstractStateChangeAspectTest extends StateChangeTest {

    private static final String STATE_FIELD_NAME = "state";

    private static final String STATE_FIELD_VALUE = "someState";

    @Test
    public final void checkPrecedencePointcutDefinitions() {
        assertEquals("com.qcadoo.mes.states.aop.StateChangePhaseAspect", StateChangePhaseAspect.class.getCanonicalName());
        assertEquals("com.qcadoo.mes.states.aop.RunInPhaseAspect", RunInPhaseAspect.class.getCanonicalName());
    }

    @Aspect
    public static class TestStateChangeAspect extends AbstractStateListenerAspect {

        @RunInPhase(1)
        @org.aspectj.lang.annotation.Before("changeStateExecution(stateChangeContext)")
        public void markEntityBefore(final StateChangeContext stateChangeContext) {
            final Entity stateChange = stateChangeContext.getStateChangeEntity();
            stateChange.setField("marked", true);
        }

        @Pointcut("this(TestStateChangeService)")
        protected void targetServicePointcut() {
        }

    }

    @Aspect
    public static class TestStateChangeService extends AbstractStateChangeAspect {

        @Override
        public void changeState(final StateChangeContext stateChangeContext) {
            final StateChangeEntityDescriber describer = stateChangeContext.getDescriber();
            final Entity stateChangeEntity = stateChangeContext.getStateChangeEntity();
            Entity targetEntity = stateChangeEntity.getBelongsToField(describer.getOwnerFieldName());
            targetEntity.setField(describer.getOwnerStateFieldName(),
                    stateChangeEntity.getField(describer.getTargetStateFieldName()));
        }

        @Override
        public StateChangeEntityDescriber getChangeEntityDescriber() {
            return new MockStateChangeDescriber();
        }

        @Override
        protected void changeStatePhase(final StateChangeContext stateChangeContext, final int phaseNumber) {
        }

    }

    @Aspect
    public static class AnotherStateChangeService extends AbstractStateChangeAspect {

        @Override
        public void changeState(final StateChangeContext stateChangeContext) {
            final StateChangeEntityDescriber describer = stateChangeContext.getDescriber();
            final Entity stateChangeEntity = stateChangeContext.getStateChangeEntity();
            Entity targetEntity = stateChangeEntity.getBelongsToField(describer.getOwnerFieldName());
            targetEntity.setField(describer.getOwnerStateFieldName(),
                    stateChangeEntity.getField(describer.getTargetStateFieldName()));
        }

        @Override
        protected void changeStatePhase(final StateChangeContext stateChangeContext, final int phaseNumber) {
        }

        @Override
        public StateChangeEntityDescriber getChangeEntityDescriber() {
            return new MockStateChangeDescriber();
        }

    }

    @Aspect
    public static class AlmostRealStateChangeService extends AbstractStateChangeAspect {

        @Override
        public StateChangeEntityDescriber getChangeEntityDescriber() {
            return new MockStateChangeDescriber();
        }

        @Override
        protected void changeStatePhase(final StateChangeContext stateChangeContext, final int phaseNumber) {
        }

    }

    @Aspect
    public static class ExceptionThrowingStateChangeService extends AbstractStateChangeAspect {

        @Override
        public StateChangeEntityDescriber getChangeEntityDescriber() {
            return new MockStateChangeDescriber();
        }

        @Override
        protected void changeStatePhase(final StateChangeContext stateChangeContext, final int phaseNumber) {
            throw new RuntimeException();
        }
    }

    @Mock
    private Entity ownerEntity;

    @Mock
    private DataDefinition dataDefinition;

    @Before
    public void init() {
        MockitoAnnotations.initMocks(this);
        stubStateChangeEntity(DESCRIBER);
        EntityList emptyEntityList = mockEmptyEntityList();
        stubStateChangeContext();
        stubOwner();
        given(stateChangeEntity.getHasManyField("messages")).willReturn(emptyEntityList);
        given(DESCRIBER.getOwnerDataDefinition().save(Mockito.any(Entity.class))).willAnswer(new Answer<Entity>() {

            @Override
            public Entity answer(final InvocationOnMock invocation) throws Throwable {
                return (Entity) invocation.getArguments()[0];
            }
        });
    }

    @Test
    public final void shouldFireListenersAndChangeState() {
        // given
        TestStateChangeService testStateChangeService = new TestStateChangeService();
        given(stateChangeEntity.getBelongsToField(DESCRIBER.getOwnerFieldName())).willReturn(ownerEntity);
        stubStringField(stateChangeEntity, DESCRIBER.getTargetStateFieldName(), STATE_FIELD_VALUE);
        stubEntityMock("", "");

        // when
        testStateChangeService.changeState(stateChangeContext);

        // then
        verify(stateChangeEntity).setField("marked", true);
        verify(ownerEntity).setField(STATE_FIELD_NAME, STATE_FIELD_VALUE);
    }

    @Test
    public final void shouldNotFireListenersForOtherStateChangeService() {
        // given
        AnotherStateChangeService anotherStateChangeService = new AnotherStateChangeService();
        given(stateChangeEntity.getBelongsToField(DESCRIBER.getOwnerFieldName())).willReturn(ownerEntity);
        stubStringField(stateChangeEntity, DESCRIBER.getTargetStateFieldName(), STATE_FIELD_VALUE);
        stubEntityMock("", "");

        // when
        anotherStateChangeService.changeState(stateChangeContext);

        // then
        verify(stateChangeEntity, never()).setField("marked", true);
        verify(ownerEntity).setField(STATE_FIELD_NAME, STATE_FIELD_VALUE);
    }

    @Test
    public final void shouldNotFireChangeStateIfStateChangeWasSuccessfulFinished() {
        // given
        AnotherStateChangeService anotherStateChangeService = new AnotherStateChangeService();
        given(stateChangeEntity.getBelongsToField(DESCRIBER.getOwnerFieldName())).willReturn(ownerEntity);
        given(stateChangeEntity.getStringField(DESCRIBER.getStatusFieldName())).willReturn(
                StateChangeStatus.SUCCESSFUL.getStringValue());
        mockStateChangeStatus(stateChangeEntity, StateChangeStatus.SUCCESSFUL);

        // when
        anotherStateChangeService.changeState(stateChangeContext);

        // then
        verify(ownerEntity, never()).setField(Mockito.eq(STATE_FIELD_NAME), Mockito.any());
    }

    @Test
    public final void shouldNotFireChangeStateIfStateChangeWasFailureFinished() {
        // given
        AnotherStateChangeService anotherStateChangeService = new AnotherStateChangeService();
        given(stateChangeEntity.getBelongsToField(DESCRIBER.getOwnerFieldName())).willReturn(ownerEntity);
        given(stateChangeEntity.getStringField(DESCRIBER.getStatusFieldName())).willReturn(
                StateChangeStatus.SUCCESSFUL.getStringValue());
        mockStateChangeStatus(stateChangeEntity, StateChangeStatus.FAILURE);

        // when
        anotherStateChangeService.changeState(stateChangeContext);

        // then
        verify(ownerEntity, never()).setField(Mockito.eq(STATE_FIELD_NAME), Mockito.any());
    }

    @Test
    public final void shouldNotFireChangeStateIfStateChangeWasPaused() {
        // given
        AnotherStateChangeService anotherStateChangeService = new AnotherStateChangeService();
        given(stateChangeEntity.getBelongsToField(DESCRIBER.getOwnerFieldName())).willReturn(ownerEntity);
        given(stateChangeEntity.getStringField(DESCRIBER.getStatusFieldName())).willReturn(
                StateChangeStatus.SUCCESSFUL.getStringValue());
        mockStateChangeStatus(stateChangeEntity, StateChangeStatus.PAUSED);

        // when
        anotherStateChangeService.changeState(stateChangeContext);

        // then
        verify(ownerEntity, never()).setField(Mockito.eq(STATE_FIELD_NAME), Mockito.any());
    }

    @Test
    public final void shouldNotFireListenersIfStateChangeWasAlreadyFinished() {
        // given
        AnotherStateChangeService anotherStateChangeService = new AnotherStateChangeService();
        given(stateChangeEntity.getBelongsToField(DESCRIBER.getOwnerFieldName())).willReturn(ownerEntity);
        given(stateChangeEntity.getBooleanField(DESCRIBER.getStatusFieldName())).willReturn(true);
        given(stateChangeEntity.getField(DESCRIBER.getStatusFieldName())).willReturn(true);

        // when
        anotherStateChangeService.changeState(stateChangeContext);

        // then
        verify(stateChangeEntity, never()).setField("marked", true);
    }

    @Test
    public final void shouldNotFireChangeStateIfStateChangeHaveErrorMessages() {
        // given
        AnotherStateChangeService anotherStateChangeService = new AnotherStateChangeService();
        EntityList messagesEntityList = mockEntityList(Lists.newArrayList(mockMessage(StateMessageType.FAILURE, "fail")));
        given(stateChangeEntity.getHasManyField(DESCRIBER.getMessagesFieldName())).willReturn(messagesEntityList);
        given(stateChangeEntity.getField(DESCRIBER.getMessagesFieldName())).willReturn(messagesEntityList);

        // when
        anotherStateChangeService.changeState(stateChangeContext);

        // then
        verify(ownerEntity, never()).setField(Mockito.eq(STATE_FIELD_NAME), Mockito.any());
    }

    @Test
    public final void shouldNotFireListenersIfStateChangeHaveErrorMessages() {
        // given
        AnotherStateChangeService anotherStateChangeService = new AnotherStateChangeService();
        EntityList messagesEntityList = mockEntityList(Lists.newArrayList(mockMessage(StateMessageType.FAILURE, "fail")));
        given(stateChangeEntity.getHasManyField(DESCRIBER.getMessagesFieldName())).willReturn(messagesEntityList);
        given(stateChangeEntity.getField(DESCRIBER.getMessagesFieldName())).willReturn(messagesEntityList);

        // when
        anotherStateChangeService.changeState(stateChangeContext);

        // then
        verify(stateChangeEntity, never()).setField("marked", true);
    }

    @Test
    public final void shouldNotPerformEntityStateChangeIfOwnerHasValidationErrors() {
        // given
        AlmostRealStateChangeService stateChangeService = new AlmostRealStateChangeService();
        given(stateChangeContext.isOwnerValid()).willReturn(true, false);

        // when
        stateChangeService.changeState(stateChangeContext);

        // then
        verify(stateChangeContext).setStatus(StateChangeStatus.FAILURE);
    }

    @Test
    public final void shouldMarkStateChangeAsFailureAndRethrowExceptionWhenStateChangePhaseThrowsException() {
        // given
        ExceptionThrowingStateChangeService stateChangeService = new ExceptionThrowingStateChangeService();
        try {
            // when
            stateChangeService.changeState(stateChangeContext);
        } catch (StateChangeException e) {
            // then
            verify(stateChangeContext).setStatus(StateChangeStatus.FAILURE);
            verify(stateChangeContext, Mockito.atLeastOnce()).save();
        }
    }

    @Test
    public final void shouldMarkStateChangeAsFailureAndRethrowExceptionWhenOwnerEntityValidatorThrowException() {
        // given
        AlmostRealStateChangeService stateChangeService = new AlmostRealStateChangeService();
        given(ownerDD.callValidators(Mockito.any(Entity.class))).willThrow(new RuntimeException());

        try {
            // when
            stateChangeService.changeState(stateChangeContext);
        } catch (StateChangeException e) {
            // then
            verify(stateChangeContext).setStatus(StateChangeStatus.FAILURE);
            verify(stateChangeContext, Mockito.atLeastOnce()).save();
        }
    }

    @Test
    public final void shouldMarkStateChangeAsFailureWhenOwnerIsInvalid() {
        // given
        AlmostRealStateChangeService stateChangeService = new AlmostRealStateChangeService();
        given(owner.isValid()).willReturn(false);
        given(ownerDD.callValidators(owner)).willReturn(false);

        // when
        stateChangeService.changeState(stateChangeContext);

        // then
        verify(stateChangeContext).setStatus(StateChangeStatus.FAILURE);
        verify(stateChangeContext, Mockito.atLeastOnce()).save();
    }

    @SuppressWarnings("unchecked")
    private EntityList mockEmptyEntityList() {
        EntityList entityList = mock(EntityList.class);
        given(entityList.isEmpty()).willReturn(true);
        given(entityList.iterator()).willReturn(Collections.EMPTY_LIST.iterator());
        return entityList;
    }

    private void stubEntityMock(final String pluginIdentifier, final String modelName) {
        given(dataDefinition.getPluginIdentifier()).willReturn(pluginIdentifier);
        given(dataDefinition.getName()).willReturn(modelName);

    }

}
