/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.cxf.systest.ws.rm;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import javax.xml.ws.BindingProvider;
import javax.xml.ws.Endpoint;

import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.bus.spring.SpringBusFactory;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.ext.logging.LoggingInInterceptor;
import org.apache.cxf.ext.logging.LoggingOutInterceptor;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.greeter_control.Greeter;
import org.apache.cxf.greeter_control.GreeterService;
import org.apache.cxf.message.Message;
import org.apache.cxf.systest.ws.util.MessageFlow;
import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase;
import org.apache.cxf.testutil.common.AbstractBusTestServerBase;
import org.apache.cxf.testutil.recorders.InMessageRecorder;
import org.apache.cxf.testutil.recorders.MessageRecorder;
import org.apache.cxf.testutil.recorders.OutMessageRecorder;
import org.apache.cxf.ws.addressing.VersionTransformer.Names200408;
import org.apache.cxf.ws.rm.DestinationSequence;
import org.apache.cxf.ws.rm.RM10Constants;
import org.apache.cxf.ws.rm.RMManager;
import org.apache.cxf.ws.rm.RMUtils;
import org.apache.cxf.ws.rm.SourceSequence;
import org.apache.cxf.ws.rm.persistence.RMMessage;
import org.apache.cxf.ws.rm.persistence.RMStore;
import org.apache.cxf.ws.rm.persistence.jdbc.RMTxStore;

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

/**
 * Tests the addition of WS-RM properties to application messages and the
 * exchange of WS-RM protocol messages.
 */
public abstract class AbstractClientPersistenceTest extends AbstractBusClientServerTestBase {
    public static final String GREETMEONEWAY_ACTION
        = "http://cxf.apache.org/greeter_control/Greeter/greetMeOneWayRequest";
    public static final String GREETME_ACTION
        = "http://cxf.apache.org/greeter_control/Greeter/greetMeRequest";
    private static final Logger LOG = LogUtils.getLogger(ClientPersistenceTest.class);

    private Greeter greeter;
    private OutMessageRecorder out;
    private InMessageRecorder in;

    public static class Server extends AbstractBusTestServerBase {
        Endpoint ep;
        String port;
        String pfx;

        public Server(String[] args) {
            port = args[0];
            pfx = args[1];
        }

        protected void run() {
            SpringBusFactory bf = new SpringBusFactory();
            System.setProperty("db.name", pfx + "-server");
            Bus bus = bf.createBus("/org/apache/cxf/systest/ws/rm/persistent.xml");
            BusFactory.setDefaultBus(bus);
            setBus(bus);
            System.clearProperty("db.name");

            LoggingInInterceptor logIn = new LoggingInInterceptor();
            bus.getInInterceptors().add(logIn);
            LoggingOutInterceptor logOut = new LoggingOutInterceptor();
            bus.getOutFaultInterceptors().add(logOut);
            bus.getOutFaultInterceptors().add(logOut);

            bus.getExtension(RMManager.class).getConfiguration()
                .setBaseRetransmissionInterval(Long.valueOf(60000));

            GreeterImpl implementor = new GreeterImpl();
            String address = "http://localhost:" + port + "/SoapContext/GreeterPort";
            ep = Endpoint.create(implementor);
            Map<String, Object> properties = new HashMap<>();
            properties.put(Message.SCHEMA_VALIDATION_ENABLED, Boolean.TRUE);
            ep.setProperties(properties);
            ep.publish(address);
            LOG.info("Published greeter endpoint.");
        }
        public void tearDown() {
            ep.stop();
            ep = null;
        }

        public static void main(String[] args) throws Exception {
            new Server(args).start();
        }

    }

    public abstract String getPort();
    public abstract String getPrefix();

    public static void startServers(String port, String pfx) throws Exception {
        RMTxStore.deleteDatabaseFiles(pfx + "-server", true);
        RMTxStore.deleteDatabaseFiles(pfx + "-client", true);
        assertTrue("server did not launch correctly",
                   launchServer(Server.class, null, new String[] {port, pfx}, true));
    }

    @Test
    public void testRecovery() throws Exception {
        startClient();
        populateStore();
        verifyStorePopulation();
        stopClient();
        startClient();
        populateStoreAfterRestart();
        recover();
        verifyRecovery();
    }

    protected void startClient() throws Exception {
        LOG.fine("Creating greeter client");
        System.setProperty("db.name", getPrefix() + "-client");
        SpringBusFactory bf = new SpringBusFactory();
        bus = bf.createBus("/org/apache/cxf/systest/ws/rm/persistent.xml");
        BusFactory.setDefaultBus(bus);
        System.clearProperty("db.name");

        GreeterService gs = new GreeterService();
        greeter = gs.getGreeterPort();
        updateAddressPort(greeter, getPort());
        ((BindingProvider)greeter).getRequestContext().put(Message.SCHEMA_VALIDATION_ENABLED, Boolean.TRUE);
        out = new OutMessageRecorder();
        in = new InMessageRecorder();

        bus.getOutInterceptors().add(out);
        bus.getInInterceptors().add(in);
    }

    protected void populateStore() throws Exception {

        bus.getExtension(RMManager.class).getConfiguration().setBaseRetransmissionInterval(Long.valueOf(60000));
        bus.getOutInterceptors().add(new MessageLossSimulator());

        greeter.greetMeOneWay("one");
        greeter.greetMeOneWay("two");
        greeter.greetMeOneWay("three");
        greeter.greetMeOneWay("four");

        awaitMessages(5, 3);

        MessageFlow mf = new MessageFlow(out.getOutboundMessages(), in.getInboundMessages(),
            Names200408.WSA_NAMESPACE_NAME, RM10Constants.NAMESPACE_URI);

        // sent create seq + 4 app messages and losing 2 app messages
        mf.verifyMessages(5, true);
        String[] expectedActions = new String[] {RM10Constants.CREATE_SEQUENCE_ACTION,
                                                 GREETMEONEWAY_ACTION,
                                                 GREETMEONEWAY_ACTION,
                                                 GREETMEONEWAY_ACTION,
                                                 GREETMEONEWAY_ACTION};
        mf.verifyActions(expectedActions, true);
        mf.verifyMessageNumbers(new String[] {null, "1", "2", "3", "4"}, true);
        mf.verifyAcknowledgements(new boolean[5], true);

        // as 2 messages being lost, received seq ack and 2 ack messages
        mf.verifyMessages(3, false);
        expectedActions = new String[] {RM10Constants.CREATE_SEQUENCE_RESPONSE_ACTION,
            RM10Constants.SEQUENCE_ACKNOWLEDGMENT_ACTION,
            RM10Constants.SEQUENCE_ACKNOWLEDGMENT_ACTION};
        mf.verifyActions(expectedActions, false);
        mf.verifyAcknowledgements(new boolean[] {false, true, true}, false);
    }

    protected void verifyStorePopulation() {

        RMManager manager = bus.getExtension(RMManager.class);
        assertNotNull(manager);
        RMStore store = manager.getStore();
        assertNotNull(store);

        Client client = ClientProxy.getClient(greeter);
        String id = RMUtils.getEndpointIdentifier(client.getEndpoint());

        Collection<DestinationSequence> dss =
            store.getDestinationSequences(id);
        assertEquals(1, dss.size());

        Collection<SourceSequence> sss =
            store.getSourceSequences(id);
        assertEquals(1, sss.size());

        Collection<RMMessage> msgs =
            store.getMessages(sss.iterator().next().getIdentifier(), true);
        assertEquals(2, msgs.size());

        msgs =
            store.getMessages(sss.iterator().next().getIdentifier(), false);
        assertEquals(0, msgs.size());
    }

    protected void stopClient() {
        // ClientProxy.getClient(greeter).destroy();
        bus.shutdown(true);
    }

    protected void populateStoreAfterRestart() throws Exception {

        bus.getExtension(RMManager.class).getConfiguration().setBaseRetransmissionInterval(Long.valueOf(60000));

        greeter.greetMeOneWay("five");

        // force at least two outbound messages, since can't always count on three
        awaitMessages(1, 2);

        MessageFlow mf = new MessageFlow(out.getOutboundMessages(), in.getInboundMessages(),
            Names200408.WSA_NAMESPACE_NAME, RM10Constants.NAMESPACE_URI);

        // sent 1 app message and no create seq messag this time
        mf.verifyMessages(1, true);
        String[] expectedActions = new String[] {GREETMEONEWAY_ACTION};

        mf.verifyActions(expectedActions, true);
        mf.verifyMessageNumbers(new String[] {"5"}, true);
        mf.verifyAcknowledgements(new boolean[1], true);

        // need in-exact as it COULD be 3 acks on a slow machine
        //normally it will ack 1,3,5 and then 1-5, but on a slow machine,
        //I've seen 1,3,5, then 1-3,5, and then 1-5
        mf.verifyMessages(2, false, false);

        // we can't reliably predict how the three remaining messages are acknowledged
//        expectedActions = new String[] {RM10Constants.SEQUENCE_ACKNOWLEDGMENT_ACTION,
//            RM10Constants.SEQUENCE_ACKNOWLEDGMENT_ACTION,
//            null};
//        mf.verifyActions(expectedActions, false);
//        mf.verifyAcknowledgements(new boolean[]{true, true, false}, false);

        // verify the final ack range to be complete
        try {
            mf.verifyAcknowledgementRange(1, 5);
        } catch (AssertionError er) {
            //possibly only got the first 2 ranges when split in 3, lets
            //wait for the third and then recheck
            awaitMessages(1, 3);
            mf.reset(out.getOutboundMessages(), in.getInboundMessages());
            mf.verifyAcknowledgementRange(1, 5);
        }
    }

    protected void recover() throws Exception {

        // do nothing - resends should happen in the background

        Thread.sleep(5000);
        LOG.info("Recovered messages should have been resent by now.");

    }

    protected void verifyRecovery() throws Exception {

        RMManager manager = bus.getExtension(RMManager.class);
        assertNotNull(manager);

        RMStore store = manager.getStore();
        assertNotNull(store);

        Client client = ClientProxy.getClient(greeter);
        String id = RMUtils.getEndpointIdentifier(client.getEndpoint());

        Collection<DestinationSequence> dss =
            store.getDestinationSequences(id);
        assertEquals(1, dss.size());

        Collection<SourceSequence> sss =
            store.getSourceSequences(id);
        assertEquals(1, sss.size());

        int i = 0;
        while (store.getMessages(sss.iterator().next().getIdentifier(), true).size() > 0 && i < 10) {
            Thread.sleep(200);
            i++;
        }

        assertEquals(0, store.getMessages(sss.iterator().next().getIdentifier(), true).size());
        assertEquals(0, store.getMessages(sss.iterator().next().getIdentifier(), false).size());
    }

    private void awaitMessages(int nExpectedOut, int nExpectedIn) {
        awaitMessages(nExpectedOut, nExpectedIn, 20000);
    }

    private void awaitMessages(int nExpectedOut, int nExpectedIn, int timeout) {
        MessageRecorder mr = new MessageRecorder(out, in);
        mr.awaitMessages(nExpectedOut, nExpectedIn, timeout);
    }

}
