/**
 * ***************************************************************************
 * 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.technologies.tree.builder;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;

import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.test.util.ReflectionTestUtils;

import com.google.common.collect.Lists;
import com.qcadoo.mes.basic.constants.BasicConstants;
import com.qcadoo.mes.technologies.constants.OperationProductInComponentFields;
import com.qcadoo.mes.technologies.constants.TechnologiesConstants;
import com.qcadoo.mes.technologies.constants.TechnologyOperationComponentFields;
import com.qcadoo.mes.technologies.tree.builder.api.InternalOperationProductComponent;
import com.qcadoo.mes.technologies.tree.builder.api.InternalTechnologyOperationComponent;
import com.qcadoo.mes.technologies.tree.builder.api.ItemWithQuantity;
import com.qcadoo.mes.technologies.tree.builder.api.OperationProductComponent;
import com.qcadoo.mes.technologies.tree.builder.api.OperationProductComponent.OperationCompType;
import com.qcadoo.mes.technologies.tree.builder.api.TechnologyOperationComponent;
import com.qcadoo.mes.technologies.tree.builder.api.TechnologyTreeAdapter;
import com.qcadoo.model.api.DataDefinition;
import com.qcadoo.model.api.Entity;
import com.qcadoo.model.api.EntityTree;
import com.qcadoo.model.api.EntityTreeNode;
import com.qcadoo.model.api.NumberService;

public class TechnologyTreeBuildServiceImplTest {

    private TechnologyTreeBuildServiceImpl treeBuildService;

    @Mock
    private TechnologyTreeComponentsFactory componentsFactory;

    @Mock
    private Entity toc2op, toc2opic1prod, toc2opoc1prod;

    @Mock
    private Entity toc1op, toc1opic1prod, toc1opoc1prod;

    @Mock
    private NumberService numberService;

    @Captor
    private ArgumentCaptor<Collection<Entity>> toc1opicCaptor, toc1opocCaptor, toc2opicCaptor, toc2opocCaptor,
            toc1childrenCaptor, toc2childrenCaptor;

    @Before
    public final void init() {
        MockitoAnnotations.initMocks(this);
        treeBuildService = new TechnologyTreeBuildServiceImpl();
        given(componentsFactory.buildToc()).willAnswer(new Answer<InternalTechnologyOperationComponent>() {

            @Override
            public InternalTechnologyOperationComponent answer(final InvocationOnMock invocation) throws Throwable {
                return buildToc();
            }
        });
        given(componentsFactory.buildOpc(Mockito.any(OperationCompType.class))).willAnswer(
                new Answer<InternalOperationProductComponent>() {

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

        ReflectionTestUtils.setField(treeBuildService, "componentsFactory", componentsFactory);
        ReflectionTestUtils.setField(treeBuildService, "numberService", numberService);
    }

    private InternalOperationProductComponent buildOpc(final OperationCompType type) {
        DataDefinition dataDef = mock(DataDefinition.class);
        given(dataDef.getPluginIdentifier()).willReturn(TechnologiesConstants.PLUGIN_IDENTIFIER);
        given(dataDef.getName()).willReturn(type.getModelName());
        Entity wrappedEntity = mock(Entity.class);
        given(dataDef.create()).willReturn(wrappedEntity);
        given(wrappedEntity.copy()).willReturn(wrappedEntity);
        return new OperationProductComponentImpl(type, dataDef);
    }

    private InternalTechnologyOperationComponent buildToc() {
        DataDefinition dataDef = mock(DataDefinition.class);
        given(dataDef.getPluginIdentifier()).willReturn(TechnologiesConstants.PLUGIN_IDENTIFIER);
        given(dataDef.getName()).willReturn(TechnologiesConstants.MODEL_TECHNOLOGY_OPERATION_COMPONENT);
        Entity wrappedEntity = mock(Entity.class);
        given(dataDef.create()).willReturn(wrappedEntity);
        given(wrappedEntity.copy()).willReturn(wrappedEntity);
        return new TechnologyOperationCompImpl(dataDef);
    }

    private class TestTreeAdapter implements TechnologyTreeAdapter<TocHolder, ProductHolder> {

        @Override
        public void setOpCompCustomFields(final TechnologyOperationComponent toc, final TocHolder from) {
        }

        @Override
        public void setOpProductCompCustomFields(final OperationProductComponent opc, final ProductHolder from) {
        }

        @Override
        public Collection<ItemWithQuantity<ProductHolder>> extractInputProducts(final TocHolder from) {
            return from.getInputs();
        }

        @Override
        public Collection<ItemWithQuantity<ProductHolder>> extractOutputProducts(final TocHolder from) {
            return from.getOutputs();
        }

        @Override
        public Iterable<TocHolder> extractSubOperations(final TocHolder from) {
            return from.getSubOps();
        }

        @Override
        public Entity buildProductEntity(final ProductHolder from) {
            return from.getProduct();
        }

        @Override
        public Entity buildOperationEntity(final TocHolder from) {
            return from.getOperation();
        }

    }

    private interface TocHolder {

        Collection<ItemWithQuantity<ProductHolder>> getInputs();

        Collection<ItemWithQuantity<ProductHolder>> getOutputs();

        Entity getOperation();

        Iterable<TocHolder> getSubOps();
    }

    private interface ProductHolder {

        Entity getProduct();
    }

    private TocHolder mockCustomTreeRepresentation() {
        DataDefinition toc2opDataDef = mockOperationDataDef();
        given(toc2op.getDataDefinition()).willReturn(toc2opDataDef);

        DataDefinition toc1opDataDef = mockOperationDataDef();
        given(toc1op.getDataDefinition()).willReturn(toc1opDataDef);

        ItemWithQuantity<ProductHolder> toc2input1 = mockProduct(toc2opic1prod, BigDecimal.ONE);
        Collection<ItemWithQuantity<ProductHolder>> toc2inputs = Lists.newArrayList();
        toc2inputs.add(toc2input1);

        ItemWithQuantity<ProductHolder> toc2output1 = mockProduct(toc2opoc1prod, BigDecimal.ONE);
        Collection<ItemWithQuantity<ProductHolder>> toc2outputs = Lists.newArrayList();
        toc2outputs.add(toc2output1);

        TocHolder toc2 = mockTocNode(toc2op, toc2inputs, toc2outputs, Collections.<TocHolder> emptyList());

        ItemWithQuantity<ProductHolder> toc1input1 = mockProduct(toc1opic1prod, BigDecimal.ONE);
        Collection<ItemWithQuantity<ProductHolder>> toc1inputs = Lists.newArrayList();
        toc1inputs.add(toc1input1);

        ItemWithQuantity<ProductHolder> toc1output1 = mockProduct(toc1opoc1prod, BigDecimal.ONE);
        Collection<ItemWithQuantity<ProductHolder>> toc1outputs = Lists.newArrayList();
        toc1outputs.add(toc1output1);

        return mockTocNode(toc1op, toc1inputs, toc1outputs, Lists.newArrayList(toc2));
    }

    private ItemWithQuantity<ProductHolder> mockProduct(final Entity productEntity, final BigDecimal quantity) {
        ProductHolder product = mock(ProductHolder.class);
        given(product.getProduct()).willReturn(productEntity);
        given(numberService.setScaleWithDefaultMathContext(quantity)).willReturn(quantity);
        DataDefinition dataDef = mockProductDataDef();
        given(productEntity.getDataDefinition()).willReturn(dataDef);
        return new ItemWithQuantity<ProductHolder>(product, quantity, 0);
    }

    private DataDefinition mockOperationDataDef() {
        return mockDataDefinition(TechnologiesConstants.PLUGIN_IDENTIFIER, TechnologiesConstants.MODEL_OPERATION);
    }

    private DataDefinition mockProductDataDef() {
        return mockDataDefinition(BasicConstants.PLUGIN_IDENTIFIER, BasicConstants.MODEL_PRODUCT);
    }

    private DataDefinition mockDataDefinition(final String pluginId, final String modelName) {
        DataDefinition dataDefinition = mock(DataDefinition.class);
        given(dataDefinition.getPluginIdentifier()).willReturn(pluginId);
        given(dataDefinition.getName()).willReturn(modelName);
        return dataDefinition;
    }

    private TocHolder mockTocNode(final Entity operation, final Collection<ItemWithQuantity<ProductHolder>> inputs,
            final Collection<ItemWithQuantity<ProductHolder>> outputs, final Iterable<TocHolder> subOps) {
        TocHolder tocNode = mock(TocHolder.class);
        given(tocNode.getOperation()).willReturn(operation);
        given(tocNode.getInputs()).willReturn(inputs);
        given(tocNode.getOutputs()).willReturn(outputs);
        given(tocNode.getSubOps()).willReturn(subOps);
        return tocNode;
    }

    @Test
    public final void shouldBuildTree() {
        // given
        TocHolder customTreeRoot = mockCustomTreeRepresentation();

        // when
        EntityTree tree = treeBuildService.build(customTreeRoot, new TestTreeAdapter());

        // then
        EntityTreeNode root = tree.getRoot();
        assertNotNull(root);
        Entity toc1 = extractEntity(root);
        verify(toc1).setField(TechnologyOperationComponentFields.OPERATION, toc1op);

        verify(toc1).setField(Mockito.eq(TechnologyOperationComponentFields.OPERATION_PRODUCT_IN_COMPONENTS),
                toc1opicCaptor.capture());
        Collection<Entity> toc1opics = toc1opicCaptor.getValue();
        assertEquals(1, toc1opics.size());
        Entity toc1opic = toc1opics.iterator().next();
        verify(toc1opic).setField(OperationProductInComponentFields.QUANTITY, BigDecimal.ONE);
        verify(toc1opic).setField(OperationProductInComponentFields.PRODUCT, toc1opic1prod);

        verify(toc1).setField(Mockito.eq(TechnologyOperationComponentFields.OPERATION_PRODUCT_OUT_COMPONENTS),
                toc1opocCaptor.capture());
        Collection<Entity> toc1opocs = toc1opocCaptor.getValue();
        assertEquals(1, toc1opocs.size());
        Entity toc1opoc = toc1opocs.iterator().next();
        verify(toc1opoc).setField(OperationProductInComponentFields.QUANTITY, BigDecimal.ONE);
        verify(toc1opoc).setField(OperationProductInComponentFields.PRODUCT, toc1opoc1prod);

        verify(toc1).setField(Mockito.eq(TechnologyOperationComponentFields.CHILDREN), toc1childrenCaptor.capture());
        Collection<Entity> toc1children = toc1childrenCaptor.getValue();
        assertEquals(1, toc1children.size());

        Entity toc2 = toc1children.iterator().next();

        verify(toc2).setField(TechnologyOperationComponentFields.OPERATION, toc2op);

        verify(toc2).setField(Mockito.eq(TechnologyOperationComponentFields.OPERATION_PRODUCT_IN_COMPONENTS),
                toc2opicCaptor.capture());
        Collection<Entity> toc2opics = toc2opicCaptor.getValue();
        assertEquals(1, toc2opics.size());
        Entity toc2opic = toc2opics.iterator().next();
        verify(toc2opic).setField(OperationProductInComponentFields.QUANTITY, BigDecimal.ONE);
        verify(toc2opic).setField(OperationProductInComponentFields.PRODUCT, toc2opic1prod);

        verify(toc2).setField(Mockito.eq(TechnologyOperationComponentFields.OPERATION_PRODUCT_OUT_COMPONENTS),
                toc2opocCaptor.capture());
        Collection<Entity> toc2opocs = toc2opocCaptor.getValue();
        assertEquals(1, toc2opocs.size());
        Entity toc2opoc = toc2opocs.iterator().next();
        verify(toc2opoc).setField(OperationProductInComponentFields.QUANTITY, BigDecimal.ONE);
        verify(toc2opoc).setField(OperationProductInComponentFields.PRODUCT, toc2opoc1prod);

        verify(toc2).setField(Mockito.eq(TechnologyOperationComponentFields.CHILDREN), toc2childrenCaptor.capture());
        Collection<Entity> toc2children = toc2childrenCaptor.getValue();
        assertTrue(toc2children.isEmpty());
    }

    private Entity extractEntity(final Entity wrapper) {
        return (Entity) ReflectionTestUtils.getField(wrapper, "entity");
    }

}
