/**
 * ***************************************************************************
 * 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;

import static com.google.common.collect.Lists.newArrayList;
import static com.qcadoo.mes.technologies.constants.TechnologyOperationComponentFields.NODE_NUMBER;
import static com.qcadoo.mes.technologies.constants.TechnologyOperationComponentFields.OPERATION_PRODUCT_IN_COMPONENTS;
import static com.qcadoo.mes.technologies.constants.TechnologyOperationComponentFields.OPERATION_PRODUCT_OUT_COMPONENTS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
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 java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.test.util.ReflectionTestUtils;

import com.google.common.collect.ImmutableList;
import com.qcadoo.model.api.DataDefinition;
import com.qcadoo.model.api.DataDefinitionService;
import com.qcadoo.model.api.Entity;
import com.qcadoo.model.api.EntityList;
import com.qcadoo.model.api.EntityTree;
import com.qcadoo.model.api.EntityTreeNode;

public class TechnologyTreeValidationServiceImplTest {

    private TechnologyTreeValidationService technologyTreeValidationService;

    private static final List<EntityTreeNode> EMPTY_TREE_NODES_LIST = new ImmutableList.Builder<EntityTreeNode>().build();

    @Mock
    private DataDefinitionService dataDefinitionService;

    @Mock
    private DataDefinition dataDefinition;

    @Mock
    private EntityTree tree;

    private Map<String, Set<String>> resultMap;

    @Before
    public final void init() {
        MockitoAnnotations.initMocks(this);
        technologyTreeValidationService = new TechnologyTreeValidationServiceImpl();
        given(tree.isEmpty()).willReturn(false);
        resultMap = null;

        ReflectionTestUtils.setField(technologyTreeValidationService, "dataDefinitionService", dataDefinitionService);

        given(dataDefinitionService.get("basic", "product")).willReturn(dataDefinition);
    }

    @Test
    public final void shouldReturnEmptyMapForNullTree() {
        // when
        resultMap = technologyTreeValidationService.checkConsumingManyProductsFromOneSubOp(null);

        // then
        assertNotNull(resultMap);
        assertTrue(resultMap.isEmpty());
    }

    @Test
    public final void shouldReturnEmptyMapForEmptyTree() {
        // given
        given(tree.isEmpty()).willReturn(true);

        // when
        resultMap = technologyTreeValidationService.checkConsumingManyProductsFromOneSubOp(tree);

        // then
        assertNotNull(resultMap);
        assertTrue(resultMap.isEmpty());
    }

    @Test
    public final void shouldReturnNotEmptyMapIfParentOpConsumeManyOutputsFromOneSubOp() {
        // given
        Entity product1 = mockProductComponent(1L);
        Entity product2 = mockProductComponent(2L);
        Entity product3 = mockProductComponent(3L);
        Entity product4 = mockProductComponent(4L);
        Entity product5 = mockProductComponent(5L);
        Entity product6 = mockProductComponent(6L);

        EntityTreeNode node3 = mockOperationComponent("3.", newArrayList(product6), newArrayList(product3, product4, product5));
        EntityTreeNode node2 = mockOperationComponent("2.", newArrayList(product3, product4), newArrayList(product2),
                newArrayList(node3));
        EntityTreeNode node1 = mockOperationComponent("1.", newArrayList(product2), newArrayList(product1), newArrayList(node2));
        given(tree.getRoot()).willReturn(node1);

        // when
        resultMap = technologyTreeValidationService.checkConsumingManyProductsFromOneSubOp(tree);

        // then
        assertNotNull(resultMap);
        assertFalse(resultMap.isEmpty());
        assertEquals(1, resultMap.size());
        hasNodeNumbersFor(node2, node3);
    }

    @Test
    public final void shouldReturnNotEmptyMapIfSubOpsProduceTheSameOutputsWhichAreConsumed() {
        // given
        Entity product1 = mockProductComponent(1L);
        Entity product2 = mockProductComponent(2L);
        Entity product3 = mockProductComponent(3L);
        Entity product4 = mockProductComponent(4L);
        Entity product5 = mockProductComponent(5L);
        Entity product6 = mockProductComponent(6L);

        EntityTreeNode node3 = mockOperationComponent(3L, "3.", newArrayList(product5), newArrayList(product2, product3));
        EntityTreeNode node2 = mockOperationComponent(2L, "2.", newArrayList(product6), newArrayList(product2, product4));
        EntityTreeNode node1 = mockOperationComponent(1L, "1.", newArrayList(product2), newArrayList(product1),
                newArrayList(node2, node3));
        given(tree.getRoot()).willReturn(node1);

        // when
        Map<String, Set<Entity>> returnedMap = technologyTreeValidationService
                .checkConsumingTheSameProductFromManySubOperations(tree);

        // then
        assertNotNull(returnedMap);
        assertFalse(returnedMap.isEmpty());
        assertEquals(1, returnedMap.size());
        assertTrue(returnedMap.containsKey("1."));
        assertTrue(returnedMap.get("1.").contains(product2));
    }

    @Test
    public final void shouldReturnNotEmptyMapIfManyParentOpConsumesManyOutputsFromOneSubOp() {
        // given
        Entity product1 = mockProductComponent(1L);
        Entity product2 = mockProductComponent(2L);
        Entity product3 = mockProductComponent(3L);
        Entity product4 = mockProductComponent(4L);
        Entity product5 = mockProductComponent(5L);
        Entity product6 = mockProductComponent(6L);
        Entity product7 = mockProductComponent(7L);
        Entity product8 = mockProductComponent(8L);
        Entity product9 = mockProductComponent(9L);
        Entity product10 = mockProductComponent(10L);
        Entity product11 = mockProductComponent(11L);
        Entity product12 = mockProductComponent(12L);

        EntityTreeNode node1A3 = mockOperationComponent("1.A.3.", newArrayList(product11, product12), newArrayList(product10));
        EntityTreeNode node1A2 = mockOperationComponent("1.A.2.", newArrayList(product10),
                newArrayList(product7, product8, product9), newArrayList(node1A3));
        EntityTreeNode node1A1 = mockOperationComponent("1.A.1.", newArrayList(product7, product8, product9),
                newArrayList(product2, product3), newArrayList(node1A2));
        EntityTreeNode node1B1 = mockOperationComponent("1.B.1.", newArrayList(product6), newArrayList(product4, product5));
        EntityTreeNode node1 = mockOperationComponent("1.", newArrayList(product2, product3, product4, product5),
                newArrayList(product1), newArrayList(node1A1, node1B1));

        given(tree.getRoot()).willReturn(node1);

        // when
        resultMap = technologyTreeValidationService.checkConsumingManyProductsFromOneSubOp(tree);

        // then
        assertNotNull(resultMap);
        assertFalse(resultMap.isEmpty());
        assertEquals(2, resultMap.size());
        assertEquals(2, resultMap.get(node1.getStringField(NODE_NUMBER)).size());
        hasNodeNumbersFor(node1, node1A1);
        hasNodeNumbersFor(node1, node1B1);
        hasNodeNumbersFor(node1A1, node1A2);
    }

    @Test
    public final void shouldReturnEmptyMapIfParentOpNotConsumeManyOutputsFromOneSubOp() {
        // given
        Entity product1 = mockProductComponent(1L);
        Entity product2 = mockProductComponent(2L);
        Entity product3 = mockProductComponent(3L);
        Entity product4 = mockProductComponent(4L);
        Entity product5 = mockProductComponent(5L);
        Entity product6 = mockProductComponent(6L);

        EntityTreeNode node2B = mockOperationComponent("2.B.", newArrayList(product6), newArrayList(product4));
        EntityTreeNode node2A = mockOperationComponent("2.A.", newArrayList(product5), newArrayList(product3));
        EntityTreeNode node2 = mockOperationComponent("2.", newArrayList(product3, product4), newArrayList(product2),
                newArrayList(node2A, node2B));
        EntityTreeNode node1 = mockOperationComponent("1.", newArrayList(product2), newArrayList(product1), newArrayList(node2));
        given(tree.getRoot()).willReturn(node1);

        // when
        resultMap = technologyTreeValidationService.checkConsumingManyProductsFromOneSubOp(tree);

        // then
        assertNotNull(resultMap);
        assertTrue(resultMap.isEmpty());
    }

    /* MAP ASSERTION HELPERS */

    private void hasNodeNumbersFor(final Entity parentOperation, final Entity subOperation) {
        String parentOpNodeNumber = parentOperation.getStringField(NODE_NUMBER);
        String subOpNodeNumber = subOperation.getStringField(NODE_NUMBER);
        assertFalse(resultMap.get(parentOpNodeNumber).isEmpty());
        assertTrue(resultMap.get(parentOpNodeNumber).contains(subOpNodeNumber));
    }

    /* TREE MOCKING & STUBBING HELPERS */

    private EntityTreeNode mockOperationComponent(final String nodeNumber, final Collection<Entity> inputProducts,
            final Collection<Entity> outputProducts) {
        return mockOperationComponent(nodeNumber, inputProducts, outputProducts, EMPTY_TREE_NODES_LIST);
    }

    private EntityTreeNode mockOperationComponent(final Long id, final String nodeNumber, final Collection<Entity> inputProducts,
            final Collection<Entity> outputProducts) {
        return mockOperationComponent(id, nodeNumber, inputProducts, outputProducts, EMPTY_TREE_NODES_LIST);
    }

    private EntityTreeNode mockOperationComponent(final Long id, final String nodeNumber, final Collection<Entity> inputProducts,
            final Collection<Entity> outputProducts, final List<EntityTreeNode> subOperations) {
        EntityTreeNode operationComponent = mock(EntityTreeNode.class);

        given(operationComponent.getId()).willReturn(id);

        EntityList inputProductsList = mockProductComponentsList(inputProducts);
        given(operationComponent.getHasManyField(OPERATION_PRODUCT_IN_COMPONENTS)).willReturn(inputProductsList);
        given(operationComponent.getField(OPERATION_PRODUCT_IN_COMPONENTS)).willReturn(inputProductsList);

        EntityList outputProductsList = mockProductComponentsList(outputProducts);
        given(operationComponent.getHasManyField(OPERATION_PRODUCT_OUT_COMPONENTS)).willReturn(outputProductsList);
        given(operationComponent.getField(OPERATION_PRODUCT_OUT_COMPONENTS)).willReturn(outputProductsList);

        given(operationComponent.getField(NODE_NUMBER)).willReturn(nodeNumber);
        given(operationComponent.getStringField(NODE_NUMBER)).willReturn(nodeNumber);

        given(operationComponent.getChildren()).willReturn(subOperations);

        return operationComponent;
    }

    private EntityTreeNode mockOperationComponent(final String nodeNumber, final Collection<Entity> inputProducts,
            final Collection<Entity> outputProducts, final List<EntityTreeNode> subOperations) {
        return mockOperationComponent(null, nodeNumber, inputProducts, outputProducts, subOperations);
    }

    private EntityList mockProductComponentsList(final Collection<Entity> productComponents) {
        EntityList productComponentsList = mock(EntityList.class);
        given(productComponentsList.iterator()).willReturn(productComponents.iterator());
        given(productComponentsList.isEmpty()).willReturn(productComponents.isEmpty());
        return productComponentsList;
    }

    private Entity mockProductComponent(final Long productId) {
        Entity productComponent = mock(Entity.class);
        Entity product = mock(Entity.class);
        given(product.getField("id")).willReturn(productId);
        given(product.getId()).willReturn(productId);
        given(productComponent.getField("product")).willReturn(product);
        given(productComponent.getBelongsToField("product")).willReturn(product);
        given(dataDefinition.get(productId)).willReturn(productComponent);
        return productComponent;
    }

}
