/*
 * JBoss, Home of Professional Open Source
 * Copyright 2013, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.richfaces.ui.iteration;

import org.jboss.test.faces.AbstractFacesTest;
import org.richfaces.model.ExtendedDataModel;
import org.richfaces.model.SequenceDataModel;

import javax.faces.component.ContextCallback;
import javax.faces.component.UIComponent;
import javax.faces.component.UIComponentBase;
import javax.faces.component.UINamingContainer;
import javax.faces.component.html.HtmlForm;
import javax.faces.component.html.HtmlInputText;
import javax.faces.component.html.HtmlOutputText;
import javax.faces.component.visit.VisitCallback;
import javax.faces.component.visit.VisitContext;
import javax.faces.component.visit.VisitHint;
import javax.faces.component.visit.VisitResult;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.PhaseId;
import javax.faces.event.ValueChangeEvent;
import javax.faces.event.ValueChangeListener;
import javax.faces.model.ListDataModel;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author Nick Belaevski
 *
 */
public class DataAdaptorTestCase extends AbstractFacesTest {
    private static final String VAR_NAME = "item";

    private static class TestCallback {
        private int value;

        public void handle() {

        }

        public void reset() {
            value = 0;
        }

        public int getAndIncrement() {
            return value++;
        }

        public int get() {
            return value;
        }
    }

    private MockDataAdaptor mockDataAdaptor;
    private List<String> data;

    private ExtendedDataModel<String> createDataModel() {
        return new SequenceDataModel<String>(new ListDataModel<String>(new ArrayList<String>(data)));
    }

    private Object getVarValue() {
        return facesContext.getApplication().evaluateExpressionGet(facesContext, MessageFormat.format("#'{'{0}'}'", VAR_NAME),
            Object.class);
    }

    @Override
    public void setUp() throws Exception {
        super.setUp();

        data = Arrays.asList("a", "b", "c", "d");

        setupFacesRequest();

        mockDataAdaptor = new MockDataAdaptor();
        mockDataAdaptor.setDataModel(createDataModel());
        mockDataAdaptor.setVar(VAR_NAME);

        facesContext.getViewRoot().getChildren().add(mockDataAdaptor);
    }

    @Override
    public void tearDown() throws Exception {
        super.tearDown();

        data = null;
        mockDataAdaptor = null;
    }

    private void resetCallbacks(TestCallback... callbacks) {
        for (TestCallback callback : callbacks) {
            callback.reset();
            assertEquals(0, callback.get());
        }
    }

    private UIComponent createCallbackComponent(final TestCallback callback) throws Exception {
        return new HtmlOutputText() {
            private void notifyCallbacks() {
                callback.handle();
            }

            @Override
            public void processDecodes(FacesContext context) {
                super.processDecodes(context);
                notifyCallbacks();
            }

            @Override
            public void processValidators(FacesContext context) {
                super.processValidators(context);
                notifyCallbacks();
            }

            @Override
            public void processUpdates(FacesContext context) {
                super.processUpdates(context);
                notifyCallbacks();
            }
        };
    }

    public void testProcessChildren() throws Exception {

        TestCallback childCallback = new TestCallback() {
            @Override
            public void handle() {
                assertEquals(getVarValue(), data.get(getAndIncrement()));
            }
        };
        UIComponent child = createCallbackComponent(childCallback);
        child.setId("child");

        TestCallback facetCallback = new TestCallback() {
            @Override
            public void handle() {
                assertEquals(getVarValue(), data.get(getAndIncrement()));
            }
        };
        UIComponent facet = createCallbackComponent(facetCallback);
        child.getFacets().put("f", facet);
        facet.setId("facet");

        TestCallback immediateFacetCallback = new TestCallback() {
            @Override
            public void handle() {
                getAndIncrement();
                assertNull(getVarValue());
            }
        };
        UIComponent immediateFacet = createCallbackComponent(immediateFacetCallback);
        immediateFacet.setId("immediateFacet");

        mockDataAdaptor.getChildren().add(child);
        mockDataAdaptor.getFacets().put("facet", immediateFacet);

        mockDataAdaptor.processDecodes(facesContext);

        assertEquals(data.size(), facetCallback.get());
        assertEquals(data.size(), childCallback.get());
        assertEquals(1, immediateFacetCallback.get());

        resetCallbacks(childCallback, facetCallback, immediateFacetCallback);

        mockDataAdaptor.processValidators(facesContext);

        assertEquals(data.size(), facetCallback.get());
        assertEquals(data.size(), childCallback.get());
        assertEquals(1, immediateFacetCallback.get());

        resetCallbacks(childCallback, facetCallback, immediateFacetCallback);

        mockDataAdaptor.processUpdates(facesContext);

        assertEquals(data.size(), facetCallback.get());
        assertEquals(data.size(), childCallback.get());
        assertEquals(1, immediateFacetCallback.get());

        resetCallbacks(childCallback, facetCallback, immediateFacetCallback);
    }

    public void testSaveRestoreChildrenState() throws Exception {
        HtmlForm form = new HtmlForm();
        HtmlInputText input = new HtmlInputText();
        IterationStateHolderComponent stateHolder = new IterationStateHolderComponent();

        List<UIComponent> children = mockDataAdaptor.getChildren();
        children.add(form);
        form.getChildren().add(input);
        form.getFacets().put("facet", stateHolder);

        mockDataAdaptor.setRowKey(facesContext, Integer.valueOf(0));

        assertFalse(form.isSubmitted());
        assertNull(input.getSubmittedValue());
        assertNull(input.getLocalValue());
        assertTrue(input.isValid());
        assertFalse(input.isLocalValueSet());
        assertNull(stateHolder.getIterationState());

        form.setSubmitted(true);
        input.setSubmittedValue("user input");
        input.setValue("component value");
        input.setValid(false);
        input.setLocalValueSet(true);
        stateHolder.setIterationState("state");

        mockDataAdaptor.setRowKey(facesContext, Integer.valueOf(1));

        assertFalse(form.isSubmitted());
        assertNull(input.getSubmittedValue());
        assertNull(input.getLocalValue());
        assertTrue(input.isValid());
        assertFalse(input.isLocalValueSet());
        assertNull(stateHolder.getIterationState());

        input.setSubmittedValue("another input from user");
        input.setValue("123");
        assertTrue(input.isLocalValueSet());
        stateHolder.setIterationState("456");

        mockDataAdaptor.setRowKey(facesContext, Integer.valueOf(0));
        assertTrue(form.isSubmitted());
        assertEquals("user input", input.getSubmittedValue());
        assertEquals("component value", input.getLocalValue());
        assertFalse(input.isValid());
        assertTrue(input.isLocalValueSet());
        assertEquals("state", stateHolder.getIterationState());

        mockDataAdaptor.setRowKey(facesContext, Integer.valueOf(1));
        assertFalse(form.isSubmitted());
        assertEquals("another input from user", input.getSubmittedValue());
        assertEquals("123", input.getLocalValue());
        assertTrue(input.isValid());
        assertTrue(input.isLocalValueSet());
        assertEquals("456", stateHolder.getIterationState());

        mockDataAdaptor.setRowKey(facesContext, null);
        assertFalse(form.isSubmitted());
        assertNull(input.getSubmittedValue());
        assertNull(input.getLocalValue());
        assertTrue(input.isValid());
        assertFalse(input.isLocalValueSet());
        assertNull(stateHolder.getIterationState());
    }

    public void testSaveRestoreChildrenStateNestedDataAdaptors() throws Exception {
        MockDataAdaptor childAdaptor = new MockDataAdaptor();
        childAdaptor.setDataModel(createDataModel());

        HtmlInputText input = new HtmlInputText();

        mockDataAdaptor.getChildren().add(childAdaptor);
        childAdaptor.getChildren().add(input);

        Integer rowKey = Integer.valueOf(2);
        Integer childKey = Integer.valueOf(1);

        mockDataAdaptor.setRowKey(facesContext, rowKey);
        childAdaptor.setRowKey(facesContext, childKey);

        assertNull(input.getSubmittedValue());
        assertNull(input.getLocalValue());
        assertTrue(input.isValid());
        assertFalse(input.isLocalValueSet());

        input.setSubmittedValue("submittedValue");
        input.setValue("value");

        childAdaptor.setRowKey(facesContext, null);
        mockDataAdaptor.setRowKey(facesContext, Integer.valueOf(3));
        childAdaptor.setRowKey(facesContext, Integer.valueOf(0));

        assertNull(input.getSubmittedValue());
        assertNull(input.getLocalValue());
        assertFalse(input.isLocalValueSet());

        childAdaptor.setRowKey(facesContext, null);
        mockDataAdaptor.setRowKey(facesContext, rowKey);
        childAdaptor.setRowKey(facesContext, childKey);

        assertEquals("submittedValue", input.getSubmittedValue());
        assertEquals("value", input.getLocalValue());
        assertTrue(input.isValid());
        assertTrue(input.isLocalValueSet());
    }

    public void testEventsQueueing() throws Exception {
        HtmlInputText input = new HtmlInputText();

        final TestCallback testCallback = new TestCallback();
        input.addValueChangeListener(new ValueChangeListener() {
            public void processValueChange(ValueChangeEvent event) throws AbortProcessingException {
                testCallback.getAndIncrement();
                assertEquals(data.get(1), getVarValue());
            }
        });

        mockDataAdaptor.getChildren().add(input);
        mockDataAdaptor.setRowKey(facesContext, Integer.valueOf(1));
        assertEquals(data.get(1), getVarValue());

        new ValueChangeEvent(input, null, "testValue").queue();
        mockDataAdaptor.setRowKey(facesContext, null);

        facesContext.getViewRoot().broadcastEvents(facesContext, PhaseId.PROCESS_VALIDATIONS);
        assertEquals(1, testCallback.get());
    }

    public void testInvokeOnComponent() throws Exception {
        final HtmlInputText facet = new HtmlInputText();
        final HtmlInputText child = new HtmlInputText();

        mockDataAdaptor.getFacets().put("facet", facet);
        mockDataAdaptor.getChildren().add(child);

        mockDataAdaptor.setId("_data");
        facet.setId("_facet");
        child.setId("_child");

        boolean invocationResult;
        final TestCallback callback = new TestCallback();
        invocationResult = mockDataAdaptor.invokeOnComponent(facesContext, "_data", new ContextCallback() {
            public void invokeContextCallback(FacesContext context, UIComponent target) {
                callback.getAndIncrement();
                assertEquals(mockDataAdaptor, target);
                assertEquals("_data", target.getClientId());
            }
        });

        assertTrue(invocationResult);
        assertEquals(1, callback.get());
        callback.reset();

        final char separatorChar = UINamingContainer.getSeparatorChar(facesContext);
        invocationResult = mockDataAdaptor.invokeOnComponent(facesContext, "_data" + separatorChar + "_facet",
            new ContextCallback() {
                public void invokeContextCallback(FacesContext context, UIComponent target) {
                    callback.getAndIncrement();
                    assertEquals(facet, target);
                    assertEquals("_data" + separatorChar + "_facet", target.getClientId());
                }
            });

        assertTrue(invocationResult);
        assertEquals(1, callback.get());
        callback.reset();

        invocationResult = mockDataAdaptor.invokeOnComponent(facesContext, "_data" + separatorChar + "2" + separatorChar
            + "_child", new ContextCallback() {
            public void invokeContextCallback(FacesContext context, UIComponent target) {
                callback.getAndIncrement();
                assertEquals(child, target);
                assertEquals(data.get(2), getVarValue());
                assertEquals("_data" + separatorChar + "2" + separatorChar + "_child", target.getClientId());
            }
        });

        assertTrue(invocationResult);
        assertEquals(1, callback.get());
        callback.reset();

        invocationResult = mockDataAdaptor.invokeOnComponent(facesContext, "_data" + separatorChar + "100" + separatorChar
            + "_child", new ContextCallback() {
            public void invokeContextCallback(FacesContext context, UIComponent target) {
                fail();
            }
        });
        assertFalse(invocationResult);

        invocationResult = mockDataAdaptor.invokeOnComponent(facesContext, "_data" + separatorChar + "nonExistentComponent",
            new ContextCallback() {
                public void invokeContextCallback(FacesContext context, UIComponent target) {
                    fail();
                }
            });
        assertFalse(invocationResult);
    }

    public void testVisitChildren() throws Exception {
        final HtmlInputText facet = new HtmlInputText();
        final HtmlInputText child = new HtmlInputText();

        mockDataAdaptor.getFacets().put("facet", facet);
        mockDataAdaptor.getChildren().add(child);

        mockDataAdaptor.setId("_data");
        facet.setId("_facet");
        child.setId("_child");

        VisitContext fullVisitContext = VisitContext.createVisitContext(facesContext);

        final char separatorChar = UINamingContainer.getSeparatorChar(facesContext);
        final Set<String> idsToVisit = new HashSet<String>();
        idsToVisit.add("_data" + separatorChar + "_facet");
        idsToVisit.add("_data" + separatorChar + "0" + separatorChar + "_child");
        idsToVisit.add("_data" + separatorChar + "2" + separatorChar + "_child");

        VisitContext partialVisitContext = VisitContext.createVisitContext(facesContext, idsToVisit,
            EnumSet.of(VisitHint.SKIP_UNRENDERED));

        final TestCallback callback = new TestCallback();
        mockDataAdaptor.visitTree(fullVisitContext, new VisitCallback() {
            public VisitResult visit(VisitContext context, UIComponent target) {
                callback.getAndIncrement();
                assertNotNull(target);

                return VisitResult.ACCEPT;
            }
        });

        assertEquals(1 /* adaptor itself */+ 1 /* facet */+ data.size(), callback.get());

        callback.reset();

        mockDataAdaptor.visitTree(partialVisitContext, new VisitCallback() {
            public VisitResult visit(VisitContext context, UIComponent target) {
                callback.getAndIncrement();
                assertNotNull(target);
                assertTrue(idsToVisit.contains(target.getClientId()));
                return VisitResult.ACCEPT;
            }
        });

        assertEquals(idsToVisit.size(), callback.get());

        callback.reset();

        mockDataAdaptor.visitTree(fullVisitContext, new VisitCallback() {
            public VisitResult visit(VisitContext context, UIComponent target) {
                callback.getAndIncrement();

                if (child.equals(target)
                    && child.getClientId().equals("_data" + separatorChar + "1" + separatorChar + "_child")) {
                    return VisitResult.COMPLETE;
                }

                return VisitResult.ACCEPT;
            }
        });

        assertEquals(1 /* data adaptor */+ 1 /* facet */+ 2 /* [0..1] children */, callback.get());
    }
}

class IterationStateHolderComponent extends UIComponentBase implements IterationStateHolder {
    private Object iterationState;

    @Override
    public String getFamily() {
        return "test.Component";
    }

    public Object getIterationState() {
        return iterationState;
    }

    public void setIterationState(Object state) {
        iterationState = state;
    }
}
