/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program 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 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 Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.xforms.state;

import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import org.orbeon.oxf.pipeline.api.ExternalContext;
import org.orbeon.oxf.processor.test.TestExternalContext;
import org.orbeon.oxf.test.ResourceManagerTestBase;
import org.orbeon.oxf.util.NetUtils;
import org.orbeon.oxf.util.SecureUtils;
import org.orbeon.oxf.xforms.XFormsContainingDocument;
import org.orbeon.oxf.xforms.XFormsProperties;
import org.orbeon.oxf.xforms.XFormsStaticState;
import org.orbeon.oxf.xforms.analysis.XFormsStaticStateTest;
import org.orbeon.oxf.xforms.event.ClientEvents;
import org.orbeon.oxf.xforms.event.XFormsEvent;
import org.orbeon.oxf.xforms.event.XFormsEventTarget;
import org.orbeon.oxf.xforms.event.events.XXFormsValueEvent;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;

import static org.junit.Assert.*;

public class XFormsStateManagerTest extends ResourceManagerTestBase {

    private XFormsStateLifecycle stateManager = XFormsStateManager.instance();

    @Test
    public void testClientNoCache() {
        testClient(false, "client-nocache.xhtml");
    }

    @Test
    public void testClientDoCache() {
        testClient(true, "client-cache.xhtml");
    }

    @Test
    public void testServerNoCache() {
        testServer(false, "server-nocache.xhtml");
    }

    @Test
    public void testServerDoCache() {
        testServer(true, "server-cache.xhtml");
    }

    @Test
    public void testDocumentCacheSessionListener() {

        // Create document
        final ExternalContext.Session session = NetUtils.getSession(true);
        final XFormsStaticState staticState = XFormsStaticStateTest.getStaticState("oxf:/org/orbeon/oxf/xforms/state/server-cache.xhtml");
        final XFormsContainingDocument document = new XFormsContainingDocument(staticState, null, null, true);

        stateManager.afterInitialResponse(document, null);

        // Check there is a state manager session listener for this document
        assertNotNull(session.getAttributesMap().get(XFormsStateManager.getListenerSessionKey(document.getUUID())));

        // Test that the document is in cache
        assertSame(document, XFormsDocumentCache.instance().takeDocument(document.getUUID()));

        // Expire session
        ((TestExternalContext.TestSession) session).expireSession();

        // Test that the document is no longer in cache
        assertNull(XFormsDocumentCache.instance().takeDocument(document.getUUID()));
    }

    private static class State {
        public XFormsContainingDocument document;
        public String uuid;
        public String staticStateString;
        public String dynamicStateString;
    }

    private interface EventCallback {
        List<XFormsEvent> createEvents(XFormsContainingDocument document);
    }

    private void testClient(boolean isCache, String formFile) {

        final XFormsStaticState staticState = XFormsStaticStateTest.getStaticState("oxf:/org/orbeon/oxf/xforms/state/" + formFile);

        // Initialize document and get initial document state
        final State state1 = new State();
        {
            NetUtils.getSession(true); // make sure a session is in place as it is used by the state manager
            state1.document = new XFormsContainingDocument(staticState, null, null, true);

            state1.uuid = state1.document.getUUID();
            state1.staticStateString = stateManager.getClientEncodedStaticState(state1.document);
            state1.dynamicStateString = stateManager.getClientEncodedDynamicState(state1.document);

            assertEquals(state1.uuid.length(), SecureUtils.HexIdLength());
            assertNotNull(StringUtils.trimToNull(state1.staticStateString));
            assertNotNull(StringUtils.trimToNull(state1.dynamicStateString));

            // Initial response sent
            state1.document.afterInitialResponse();
            stateManager.afterInitialResponse(state1.document, null);
        }

        assertEquals(1, getSequenceNumber(state1.dynamicStateString));

        // Run update
        final State state2 = doUpdate(isCache, state1, new EventCallback() {
            public List<XFormsEvent> createEvents(XFormsContainingDocument document) {
                final XFormsEventTarget input = (XFormsEventTarget) document.getObjectByEffectiveId("my-input");
                return Collections.<XFormsEvent>singletonList(new XXFormsValueEvent(input, "gaga"));
            }
        });

        // UUID and static state can't change
        assertEquals(state1.uuid, state2.uuid);
        assertEquals(state1.staticStateString, state2.staticStateString);
        // Sequence must be updated
        assertEquals(2, getSequenceNumber(state2.dynamicStateString));
        // Dynamic state must have changed
        assertFalse(stripSequenceNumber(state1.dynamicStateString).equals(stripSequenceNumber(state2.dynamicStateString)));

        // Run update
        final State state3 = doUpdate(isCache, state2, null);

        // UUID and static state can't change
        assertEquals(state1.uuid, state3.uuid);
        assertEquals(state1.staticStateString, state3.staticStateString);
        // Sequence must be updated
        assertEquals(3, getSequenceNumber(state3.dynamicStateString));
        // Dynamic state must NOT have changed because no event was dispatched
        assertEquals(stripSequenceNumber(state2.dynamicStateString), stripSequenceNumber(state3.dynamicStateString));

        // Get back to initial state
        final State state4 = getInitialState(state1);

        // UUID and static state can't change
        assertEquals(state1.uuid, state4.uuid);
        assertEquals(state1.staticStateString, state4.staticStateString);
        // Sequence must be updated
        assertEquals(1, getSequenceNumber(state4.dynamicStateString));
        // Make sure we found the initial dynamic state
        assertEquals(stripSequenceNumber(state1.dynamicStateString), stripSequenceNumber(state4.dynamicStateString));
    }

    private void testServer(boolean isCache, String formFile) {

        final XFormsStaticState staticState = XFormsStaticStateTest.getStaticState("oxf:/org/orbeon/oxf/xforms/state/" + formFile);

        // Initialize document and get initial document state
        final String initialDynamicStateString;
        final State state1 = new State();
        {
            NetUtils.getSession(true);
            state1.document = new XFormsContainingDocument(staticState, null, null, true);

            state1.uuid = state1.document.getUUID();
            state1.staticStateString = stateManager.getClientEncodedStaticState(state1.document);
            state1.dynamicStateString = stateManager.getClientEncodedDynamicState(state1.document);

            assertEquals(state1.uuid.length(), SecureUtils.HexIdLength());
            assertNull(StringUtils.trimToNull(state1.staticStateString));
            assertNull(StringUtils.trimToNull(state1.dynamicStateString));

            // Initial response sent
            state1.document.afterInitialResponse();
            stateManager.afterInitialResponse(state1.document, null);

            initialDynamicStateString = DynamicState.encodeDocumentToString(state1.document, XFormsProperties.isGZIPState(), false);
        }

        assertEquals(1, state1.document.getSequence());

        // Run update
        final State state2 = doUpdate(isCache, state1, new EventCallback() {
            public List<XFormsEvent> createEvents(XFormsContainingDocument document) {
                final XFormsEventTarget input = (XFormsEventTarget) document.getObjectByEffectiveId("my-input");
                return Collections.<XFormsEvent>singletonList(new XXFormsValueEvent(input, "gaga"));
            }
        });

        // UUID can't change
        assertEquals(state1.uuid, state2.uuid);
        // Sequence must be updated
        assertEquals(2, state2.document.getSequence());
        // State strings must be null
        assertNull(StringUtils.trimToNull(state2.staticStateString));
        assertNull(StringUtils.trimToNull(state2.dynamicStateString));

        // Run update
        final State state3 = doUpdate(isCache, state2, null);

        // UUID can't change
        assertEquals(state1.uuid, state3.uuid);
        // Sequence must be updated
        assertEquals(3, state3.document.getSequence());
        // State strings must be null
        assertNull(StringUtils.trimToNull(state3.staticStateString));
        assertNull(StringUtils.trimToNull(state3.dynamicStateString));

        // Get back to initial state
        final State state4 = getInitialState(state1);

        // UUID can't change
        assertEquals(state1.uuid, state4.uuid);
        // Sequence must be updated
        assertEquals(1, state4.document.getSequence());
        // State strings must be null
        assertNull(StringUtils.trimToNull(state4.staticStateString));
        assertNull(StringUtils.trimToNull(state4.dynamicStateString));
        // Make sure we found the initial dynamic state
        {
            assertEquals(stripSequenceNumber(initialDynamicStateString), stripSequenceNumber(DynamicState.encodeDocumentToString(state4.document, XFormsProperties.isGZIPState(), false)));
        }
    }

    private State doUpdate(boolean isCache, final State state1, EventCallback callback) {

        final XFormsStateLifecycle.RequestParameters parameters = new XFormsStateLifecycle.RequestParameters() {
            public String getUUID() {
                return state1.uuid;
            }

            public String getEncodedClientStaticState() {
                return state1.staticStateString;
            }

            public String getEncodedClientDynamicState() {
                return state1.dynamicStateString;
            }
        };

        // New state
        final State state2 = new State();

        final Lock lock = stateManager.acquireDocumentLock(parameters);
        if (lock == null)
            fail("Ajax update lock timeout exceeded");
        try {
            state2.document = stateManager.beforeUpdate(parameters);

            if (isCache)
                assertSame(state1.document, state2.document);// must be the same because cache is enabled and cache has room
            else
                assertNotSame(state1.document, state2.document);// can't be the same because cache is disabled

            // Run events if any
            state2.document.beforeExternalEvents(null);
            if (callback != null) {
                for (final XFormsEvent event: callback.createEvents(state2.document)) {
                    ClientEvents.processEvent(state2.document, event);
                }
            }
            state2.document.afterExternalEvents();

            stateManager.beforeUpdateResponse(state2.document, false);

            // New state
            state2.uuid = state2.document.getUUID();
            state2.staticStateString = stateManager.getClientEncodedStaticState(state2.document);
            state2.dynamicStateString = stateManager.getClientEncodedDynamicState(state2.document);

            stateManager.afterUpdateResponse(state2.document);

            stateManager.afterUpdate(state2.document, true);
        } finally {
            stateManager.releaseDocumentLock(lock);
        }

        return state2;
    }

    private State getInitialState(final State state1) {

        final XFormsStateLifecycle.RequestParameters parameters = new XFormsStateLifecycle.RequestParameters() {
            public String getUUID() {
                return state1.uuid;
            }

            public String getEncodedClientStaticState() {
                return state1.staticStateString;
            }

            public String getEncodedClientDynamicState() {
                return state1.dynamicStateString;
            }
        };

        // New state
        final State state2 = new State();

        // Find document
        state2.document = XFormsStateManager.instance().findOrRestoreDocument(parameters, true, false);

        assertNotSame(state1.document, state2.document);// can't be the same because either cache is disabled OR we create a duplicate document (could be same if state1 is initial state)

        // New state
        state2.uuid = state2.document.getUUID();
        state2.staticStateString = stateManager.getClientEncodedStaticState(state2.document);
        state2.dynamicStateString = stateManager.getClientEncodedDynamicState(state2.document);

        return state2;
    }

    private String stripSequenceNumber(String serializedState) {
        final DynamicState dynamicState = DynamicState.apply(serializedState);
        final DynamicState newDynamicState = dynamicState.copyUpdateSequence(1);

        return newDynamicState.encodeToString(XFormsProperties.isGZIPState(), false);
    }

    private long getSequenceNumber(String serializedState) {
        return DynamicState.apply(serializedState).sequence();
    }
}
