package org.jboss.narayana.rest.integration.test.functional;

import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.jboss.jbossts.star.util.TxLinkNames;
import org.jboss.jbossts.star.util.TxMediaType;
import org.jboss.jbossts.star.util.TxStatus;
import org.jboss.jbossts.star.util.TxSupport;
import org.jboss.narayana.rest.integration.ParticipantInformation;
import org.jboss.narayana.rest.integration.ParticipantResource;
import org.jboss.narayana.rest.integration.ParticipantsContainer;
import org.jboss.narayana.rest.integration.api.Aborted;
import org.jboss.narayana.rest.integration.api.Participant;
import org.jboss.narayana.rest.integration.api.Prepared;
import org.jboss.narayana.rest.integration.api.ReadOnly;
import org.jboss.narayana.rest.integration.test.common.LoggingParticipant;
import org.jboss.resteasy.plugins.server.netty.NettyJaxrsServer;
import org.jboss.resteasy.spi.ResteasyDeployment;
import org.jboss.resteasy.test.TestPortProvider;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import com.arjuna.ats.arjuna.common.Uid;

import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.Response;

/**
 *
 * @author <a href="mailto:gytis@redhat.com">Gytis Trikleris</a>
 *
 */
public final class ParticipantResourceTestCase {

    private static final String APPLICATION_ID = "org.jboss.narayana.rest.integration.test.functional.ParticipantResourceTestCase";

    private static final String BASE_URL = "http://localhost:" + TestPortProvider.getPort();

    private static final String PARTICIPANT_URL = BASE_URL + "/" + ParticipantResource.BASE_PATH_SEGMENT;

    private static NettyJaxrsServer NETTY;

    private String participantId;

    @BeforeClass
    public static void beforeClass() {
        List<String> resourceClasses = new ArrayList<String>();
        resourceClasses.add("org.jboss.narayana.rest.integration.ParticipantResource");

        ResteasyDeployment resteasyDeployment = new ResteasyDeployment();
        resteasyDeployment.setResourceClasses(resourceClasses);

        NETTY = new NettyJaxrsServer();
        NETTY.setDeployment(resteasyDeployment);
        NETTY.setPort(TestPortProvider.getPort());
        NETTY.start();
    }

    @AfterClass
    public static void afterClass() {
        NETTY.stop();
    }

    @Before
    public void before() {
        participantId = new Uid().toString();
        ParticipantsContainer.getInstance().clear();
    }

    @Test
    @SuppressWarnings("rawtypes")
    public void testRequestsToNotRegisteredParticipant() throws Exception {
        Response simpleResponse = getParticipantTerminator(participantId);
        Assert.assertEquals(404, simpleResponse.getStatus());

        Response stringResponse = getParticipantStatus(participantId);
        Assert.assertEquals(404, stringResponse.getStatus());

        stringResponse = prepareParticipant(participantId);
        Assert.assertEquals(404, stringResponse.getStatus());

        simpleResponse = forgetParticipantHeuristic(participantId);
        Assert.assertEquals(404, simpleResponse.getStatus());
    }

    @Test
    @SuppressWarnings("rawtypes")
    public void testGetTerminator() throws Exception {
        registerParticipant(participantId, new LoggingParticipant(new ReadOnly()));

        Response response = getParticipantTerminator(participantId);
        Assert.assertEquals(200, response.getStatus());

        Link link = response.getLink(TxLinkNames.TERMINATOR);
        Assert.assertNotNull(link);
        Assert.assertEquals(PARTICIPANT_URL + "/" + participantId, link.getUri().toString());
    }

    @Test
    public void testGetStatus() throws Exception {
        registerParticipant(participantId, new LoggingParticipant(new ReadOnly()));

        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);
        participantInformation.setStatus(TxStatus.TransactionPrepared.name());

        Response response = getParticipantStatus(participantId);

        Assert.assertEquals(200, response.getStatus());
        Assert.assertTrue(TxStatus.isPrepare(TxSupport.getStatus(response.readEntity(String.class))));

        participantInformation.setStatus(TxStatus.TransactionCommitted.name());
        response = ClientBuilder.newClient().target(PARTICIPANT_URL + "/" + participantId).request().get();
        Assert.assertTrue(TxStatus.isCommit(TxSupport.getStatus(response.readEntity(String.class))));
    }

    @Test
    public void testPrepareOutcomePrepared() throws Exception {
        final LoggingParticipant participant = new LoggingParticipant(new Prepared());
        registerParticipant(participantId, participant);

        Response stringResponse = prepareParticipant(participantId);

        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);

        Assert.assertEquals(200, stringResponse.getStatus());
        Assert.assertEquals(TxStatus.TransactionPrepared.name(), TxSupport.getStatus(stringResponse.readEntity(String.class)));
        Assert.assertEquals(TxStatus.TransactionPrepared.name(), participantInformation.getStatus());
        Assert.assertEquals(Arrays.asList(new String[] { "prepare" }), participant.getInvocations());
    }

    @Test
    public void testPrepareOutcomeAborted() throws Exception {
        final LoggingParticipant participant = new LoggingParticipant(new Aborted());
        registerParticipant(participantId, participant);

        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);

        Response stringResponse = prepareParticipant(participantId);

        Assert.assertEquals(409, stringResponse.getStatus());
        Assert.assertEquals(TxStatus.TransactionRolledBack.name(), TxSupport.getStatus(stringResponse.readEntity(String.class)));
        Assert.assertEquals(TxStatus.TransactionRolledBack.name(), participantInformation.getStatus());
        Assert.assertEquals(Arrays.asList(new String[] { "prepare", "rollback" }), participant.getInvocations());
        Assert.assertNull(ParticipantsContainer.getInstance().getParticipantInformation(participantId));
    }

    @Test
    public void testPrepareOutcomeReadOnly() throws Exception {
        final LoggingParticipant participant = new LoggingParticipant(new ReadOnly());
        registerParticipant(participantId, participant);

        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);

        Response stringResponse = prepareParticipant(participantId);

        Assert.assertEquals(200, stringResponse.getStatus());
        Assert.assertEquals(TxStatus.TransactionReadOnly.name(), TxSupport.getStatus(stringResponse.readEntity(String.class)));
        Assert.assertEquals(TxStatus.TransactionReadOnly.name(), participantInformation.getStatus());
        Assert.assertEquals(Arrays.asList(new String[] { "prepare" }), participant.getInvocations());
        Assert.assertNull(ParticipantsContainer.getInstance().getParticipantInformation(participantId));
    }

    @Test
    public void testPreparePreparedTransaction() throws Exception {
        final LoggingParticipant participant = new LoggingParticipant(new Prepared());
        registerParticipant(participantId, participant);

        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);
        participantInformation.setStatus(TxStatus.TransactionPrepared.name());

        Response stringResponse = prepareParticipant(participantId);

        Assert.assertEquals(412, stringResponse.getStatus());
    }

    @Test
    public void testCommit() throws Exception {
        LoggingParticipant participant = new LoggingParticipant(new Prepared());
        registerParticipant(participantId, participant);
        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);

        participantInformation.setStatus(TxStatus.TransactionPrepared.name());

        Response stringResponse = commitParticipant(participantInformation.getId());

        Assert.assertEquals(200, stringResponse.getStatus());
        Assert.assertEquals(TxStatus.TransactionCommitted.name(), participantInformation.getStatus());
        Assert.assertEquals(Arrays.asList(new String[] { "commit" }), participant.getInvocations());
        Assert.assertNull(ParticipantsContainer.getInstance().getParticipantInformation(participantId));
    }

    @Test
    public void testCommitWithoutPrepare() throws Exception {
        LoggingParticipant participant = new LoggingParticipant(new Prepared());
        registerParticipant(participantId, participant);
        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);

        Response stringResponse = commitParticipant(participantInformation.getId());

        Assert.assertEquals(412, stringResponse.getStatus());
    }

    @Test
    public void testCommitOnePhase() throws Exception {
        LoggingParticipant participant = new LoggingParticipant(new Prepared());
        registerParticipant(participantId, participant);
        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);

        Response stringResponse = commitParticipantInOnePhase(participantInformation.getId());

        Assert.assertEquals(200, stringResponse.getStatus());
        Assert.assertEquals(TxStatus.TransactionCommittedOnePhase.name(), participantInformation.getStatus());
        Assert.assertEquals(Arrays.asList(new String[] { "commitOnePhase" }), participant.getInvocations());
        Assert.assertNull(ParticipantsContainer.getInstance().getParticipantInformation(participantId));
    }

    @Test
    public void testCommitOnePhaseAfterPrepare() throws Exception {
        LoggingParticipant participant = new LoggingParticipant(new Prepared());
        registerParticipant(participantId, participant);
        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);

        participantInformation.setStatus(TxStatus.TransactionPrepared.name());

        Response stringResponse = commitParticipantInOnePhase(participantInformation.getId());

        Assert.assertEquals(412, stringResponse.getStatus());
    }

    @Test
    public void testRollback() throws Exception {
        LoggingParticipant participant = new LoggingParticipant(new Prepared());
        registerParticipant(participantId, participant);
        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);

        Response stringResponse = rollbackParticipant(participantInformation.getId());

        Assert.assertEquals(200, stringResponse.getStatus());
        Assert.assertEquals(TxStatus.TransactionRolledBack.name(), participantInformation.getStatus());
        Assert.assertEquals(Arrays.asList(new String[] { "rollback" }), participant.getInvocations());
        Assert.assertNull(ParticipantsContainer.getInstance().getParticipantInformation(participantId));
    }

    @Test
    public void testRollbackAfterPrepare() throws Exception {
        LoggingParticipant participant = new LoggingParticipant(new Prepared());
        registerParticipant(participantId, participant);
        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);

        participantInformation.setStatus(TxStatus.TransactionPrepared.name());

        Response stringResponse = rollbackParticipant(participantInformation.getId());

        Assert.assertEquals(200, stringResponse.getStatus());
        Assert.assertEquals(TxStatus.TransactionRolledBack.name(), participantInformation.getStatus());
        Assert.assertEquals(Arrays.asList(new String[] { "rollback" }), participant.getInvocations());
        Assert.assertNull(ParticipantsContainer.getInstance().getParticipantInformation(participantId));
    }

    @Test
    @SuppressWarnings("rawtypes")
    public void testForgetHeuristic() throws Exception {
        LoggingParticipant participant = new LoggingParticipant(new Prepared());
        registerParticipant(participantId, participant);
        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);

        participantInformation.setStatus(TxStatus.TransactionHeuristicRollback.name());

        Response simpleResponse = forgetParticipantHeuristic(participantId);

        Assert.assertEquals(200, simpleResponse.getStatus());
        Assert.assertNull(ParticipantsContainer.getInstance().getParticipantInformation(participantId));
    }

    @Test
    @SuppressWarnings("rawtypes")
    public void testForgetHeuristicWithoutHeuristic() throws Exception {
        LoggingParticipant participant = new LoggingParticipant(new Prepared());
        registerParticipant(participantId, participant);

        Response simpleResponse = forgetParticipantHeuristic(participantId);

        ParticipantInformation participantInformation = ParticipantsContainer.getInstance().getParticipantInformation(
                participantId);

        Assert.assertEquals(412, simpleResponse.getStatus());
        Assert.assertEquals(TxStatus.TransactionActive.name(), participantInformation.getStatus());
    }

    private void registerParticipant(final String participantId, final Participant participant)
            throws MalformedURLException {

        ParticipantInformation participantInformation = new ParticipantInformation(participantId, APPLICATION_ID, "",
                participant);
        participantInformation.setStatus(TxStatus.TransactionActive.name());
        ParticipantsContainer.getInstance().addParticipantInformation(participantId, participantInformation);
    }

    @SuppressWarnings("rawtypes")
    private Response getParticipantTerminator(final String participantId) throws Exception {
        return ClientBuilder.newClient().target(PARTICIPANT_URL + "/" + participantId).request().head();
    }

    private Response getParticipantStatus(final String participantId) throws Exception {
        return ClientBuilder.newClient().target(PARTICIPANT_URL + "/" + participantId).request().get();
    }

    private Response prepareParticipant(final String participantId) throws Exception {
        return ClientBuilder.newClient().target(PARTICIPANT_URL + "/" + participantId).request().put(
                Entity.entity(TxSupport.toStatusContent(TxStatus.TransactionPrepared.name()), TxMediaType.TX_STATUS_MEDIA_TYPE));
    }

    private Response commitParticipant(final String participantId) throws Exception {
        return ClientBuilder.newClient().target(PARTICIPANT_URL + "/" + participantId).request().put(
                Entity.entity(
                TxSupport.toStatusContent(TxStatus.TransactionCommitted.name()),
                        TxMediaType.TX_STATUS_MEDIA_TYPE));
    }

    private Response commitParticipantInOnePhase(final String participantId) throws Exception {
        return ClientBuilder.newClient().target(PARTICIPANT_URL + "/" + participantId).request().put(
                Entity.entity(
                TxSupport.toStatusContent(TxStatus.TransactionCommittedOnePhase.name()),
                        TxMediaType.TX_STATUS_MEDIA_TYPE));
    }

    private Response rollbackParticipant(final String participantId) throws Exception {
        return ClientBuilder.newClient().target(PARTICIPANT_URL + "/" + participantId).request().put(
                Entity.entity(
                TxSupport.toStatusContent(TxStatus.TransactionRolledBack.name()),
                        TxMediaType.TX_STATUS_MEDIA_TYPE));
    }

    @SuppressWarnings("rawtypes")
    private Response forgetParticipantHeuristic(final String participantId) throws Exception {
        return ClientBuilder.newClient().target(PARTICIPANT_URL + "/" + participantId).request().delete();
    }

}
