/*
 *******************************************************************************
 * Copyright (c) 2018 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * Licensed 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.eclipse.microprofile.lra.tck;

import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER;
import static org.eclipse.microprofile.lra.tck.participant.api.AfterLRAListener.AFTER_LRA_LISTENER_WORK;
import static org.eclipse.microprofile.lra.tck.participant.api.LraResource.ACCEPT_WORK;
import static org.eclipse.microprofile.lra.tck.participant.api.LraResource.LRA_RESOURCE_PATH;
import static org.eclipse.microprofile.lra.tck.participant.api.LraResource.CANCEL_PATH;
import static org.eclipse.microprofile.lra.tck.participant.api.LraResource.TIME_LIMIT;
import static org.eclipse.microprofile.lra.tck.participant.api.LraResource.TIME_LIMIT_HALF_SEC;
import static org.eclipse.microprofile.lra.tck.participant.api.LraResource.TRANSACTIONAL_WORK_PATH;
import static org.eclipse.microprofile.lra.tck.participant.api.ParticipatingTckResource.JOIN_WITH_EXISTING_LRA_PATH;
import static org.eclipse.microprofile.lra.tck.participant.api.ParticipatingTckResource.JOIN_WITH_EXISTING_LRA_PATH2;
import static org.eclipse.microprofile.lra.tck.participant.api.ParticipatingTckResource.TCK_PARTICIPANT_RESOURCE_PATH;
import static org.eclipse.microprofile.lra.tck.participant.api.AfterLRAListener.AFTER_LRA_LISTENER_PATH;
import static org.eclipse.microprofile.lra.tck.participant.api.AfterLRAParticipant.AFTER_LRA_PARTICIPANT_PATH;
import static org.eclipse.microprofile.lra.tck.participant.api.AfterLRAParticipant.AFTER_LRA_PARTICIPANT_WORK;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.net.URI;
import java.net.URISyntaxException;
import java.time.temporal.ChronoUnit;
import java.util.stream.IntStream;

import javax.inject.Inject;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;

import org.eclipse.microprofile.lra.annotation.AfterLRA;
import org.eclipse.microprofile.lra.tck.participant.api.GenericLRAException;
import org.eclipse.microprofile.lra.tck.participant.api.LraResource;
import org.eclipse.microprofile.lra.tck.participant.api.NoLRAResource;
import org.eclipse.microprofile.lra.tck.participant.api.ParticipatingTckResource;
import org.eclipse.microprofile.lra.tck.participant.api.AfterLRAListener;
import org.eclipse.microprofile.lra.tck.participant.api.AfterLRAParticipant;
import org.eclipse.microprofile.lra.tck.service.LRAMetricService;
import org.eclipse.microprofile.lra.tck.service.LRAMetricType;
import org.eclipse.microprofile.lra.tck.service.LRATestService;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(Arquillian.class)
public class TckTests extends TckTestBase {

    @Inject
    private LRAMetricService lraMetricService;

    @Inject
    private LRATestService lraTestService;
    
    private enum CompletionType {
        complete, compensate, mixed
    }

    @Deployment(name = "tcktests")
    public static WebArchive deploy() {
        return TckTestBase.deploy(TckTests.class.getSimpleName().toLowerCase());
    }

    /**
     * <p>
     * Starting LRA and cancelling it.
     * <p>
     * It's expected the LRA won't be listed in active LRAs when cancelled.
     */
    @Test
    public void cancelLRA() throws WebApplicationException {
        try {
            URI lra = lraClient.startLRA(null,lraClientId(), lraTimeout(), ChronoUnit.MILLIS);

            lraClient.cancelLRA(lra);

            assertTrue("LRA '" + lra + "' should not be active ",
                    lraClient.isLRAFinished(lra));
        } catch (GenericLRAException e) {
            e.printStackTrace();
            throw e;
        }
    }

    /**
     * <p>
     * Starting LRA and closing it.
     * <p>
     * It's expected the LRA won't be listed in active LRAs when closed.
     */
    @Test
    public void closeLRA() throws WebApplicationException {
        URI lra = lraClient.startLRA(null, lraClientId(), lraTimeout(), ChronoUnit.MILLIS);

        lraClient.closeLRA(lra);

        assertTrue("LRA '" + lra + "' should not be active anymore",
                lraClient.isLRAFinished(lra));
    }

    @Test
    public void nestedActivity() throws WebApplicationException {
        URI lra = lraClient.startLRA(null, lraClientId(), lraTimeout(), ChronoUnit.MILLIS);
        WebTarget resourcePath = tckSuiteTarget
                .path(LRA_RESOURCE_PATH).path("nestedActivity");

        Response response = null;
        try {
            response = resourcePath
                    .request()
                    .header(LRA_HTTP_CONTEXT_HEADER, lra)
                    .put(Entity.text(""));
    
            assertEquals("Response status to ' " + resourcePath.getUri() + "' does not match.",
                    Response.Status.OK.getStatusCode(), response.getStatus());
    
            Object parentId = response.getHeaders().getFirst(LRA_HTTP_CONTEXT_HEADER);
    
            assertNotNull("Expecting to get parent LRA id as response from " + resourcePath.getUri(), parentId);
            assertEquals("The nested activity should return the parent LRA id. The call to " + resourcePath.getUri(),
                    parentId, lra.toString());
    
            URI nestedLraId = URI.create(response.readEntity(String.class)); // We can keep String.class here as it is in TCK
    
            // close the LRA
            lraClient.closeLRA(lra);
    
            // validate that the nested LRA was closed

            // the resource /activities/work is annotated with Type.REQUIRED so the container should have ended it
            assertTrue("Nested LRA id '" + lra + "' should be listed in the list of the active LRAs (from call to "
                    + resourcePath.getUri() + ")", lraClient.isLRAFinished(nestedLraId));
        } finally {
            if(response != null) {
                response.close();
            }
        } 
    }

    @Test
    public void completeMultiLevelNestedActivity() throws WebApplicationException {
        multiLevelNestedActivity(CompletionType.complete, 1);
    }

    @Test
    public void compensateMultiLevelNestedActivity() throws WebApplicationException {
        multiLevelNestedActivity(CompletionType.compensate, 1);
    }

    @Test
    public void mixedMultiLevelNestedActivity() throws WebApplicationException {
        multiLevelNestedActivity(CompletionType.mixed, 2);
    }

    @Test
    public void joinLRAViaHeader() throws WebApplicationException {
        URI lra = lraClient.startLRA(null, lraClientId(), lraTimeout(), ChronoUnit.MILLIS);

        WebTarget resourcePath = tckSuiteTarget.path(LRA_RESOURCE_PATH).path(TRANSACTIONAL_WORK_PATH);
        Response response = resourcePath
                .request().header(LRA_HTTP_CONTEXT_HEADER, lra).put(Entity.text(""));

        checkStatusAndCloseResponse(Response.Status.OK, response, resourcePath);

        // validate that the implementation still knows about lraId
        assertFalse("LRA '" + lra + "' should be active as it is not closed yet.",
                lraClient.isLRAFinished(lra));

        // close the LRA
        lraClient.closeLRA(lra);
        lraTestService.waitForCallbacks(lra);
        
        // check that participant was told to complete
        assertEquals("Wrong completion count for call " + resourcePath.getUri() + ". Expecting the method LRA was completed "
                + "after joining the existing LRA " + lra, 
                1, lraMetricService.getMetric(LRAMetricType.Completed, lra, LraResource.class.getName()));
        
        // check that implementation no longer knows about lraId
        assertTrue("LRA '" + lra + "' should not be active anymore as it was closed yet.",
                lraClient.isLRAFinished(lra));
    }

    @Test
    public void join() throws WebApplicationException {
        URI lra = lraClient.startLRA(null, lraClientId(), lraTimeout(), ChronoUnit.MILLIS);
        WebTarget resourcePath = tckSuiteTarget.path(LRA_RESOURCE_PATH).path(TRANSACTIONAL_WORK_PATH);
        Response response = resourcePath
                .request().header(LRA_HTTP_CONTEXT_HEADER, lra).put(Entity.text(""));
        checkStatusAndCloseResponse(Response.Status.OK, response, resourcePath);
        lraClient.closeLRA(lra);
        assertTrue("LRA '" + lra + "' should be active as it is not closed yet.",
                lraClient.isLRAFinished(lra));
    }

    /**
     * TCK test to verify that methods annotated with {@link AfterLRA}
     * are notified correctly when an LRA terminates
     *
     * @throws InterruptedException when waiting for the finishing the callbacks is interrupted
     */
    @Test
    public void testAfterLRAParticipant() throws WebApplicationException, InterruptedException {
        URI lra = lraClient.startLRA(null, lraClientId(), lraTimeout(), ChronoUnit.MILLIS);
        WebTarget resourcePath = tckSuiteTarget.path(AFTER_LRA_PARTICIPANT_PATH).path(AFTER_LRA_PARTICIPANT_WORK);
        Response response = resourcePath
                .request().header(LRA_HTTP_CONTEXT_HEADER, lra).put(Entity.text(""));
        checkStatusAndCloseResponse(Response.Status.OK, response, resourcePath);
        lraClient.closeLRA(lra);
        lraTestService.waitForCallbacks(lra);

        // verify that the LRA is now in one of the terminal states
        assertTrue("testAfterLRAParticipant: LRA did not finish",
                lraClient.isLRAFinished(lra, lraMetricService, AfterLRAParticipant.class.getName()));

        // verify that the resource was notified of the final state of the LRA
        assertTrue("testAfterLRAParticipant: end synchronization was not invoked on resource " + resourcePath.getUri(),
                lraMetricService.getMetric(LRAMetricType.Closed, lra, AfterLRAParticipant.class.getName()) >= 1);
    }

    /**
     * TCK test to verify that methods annotated with {@link AfterLRA}
     * are notified correctly when an LRA terminates
     */
    @Test
    public void testAfterLRAListener() {
        URI lra = lraClient.startLRA(null, lraClientId(), lraTimeout(), ChronoUnit.MILLIS);
        WebTarget resourcePath = tckSuiteTarget.path(AFTER_LRA_LISTENER_PATH).path(AFTER_LRA_LISTENER_WORK);
        Response response = resourcePath
                .request().header(LRA_HTTP_CONTEXT_HEADER, lra).put(Entity.text(""));
        checkStatusAndCloseResponse(Response.Status.OK, response, resourcePath);
        lraClient.closeLRA(lra);
        lraTestService.waitForCallbacks(lra);

        // verify that the LRA is now in one of the terminal states
        assertTrue("testAfterLRAListener: LRA did not finish",
                lraClient.isLRAFinished(lra, lraMetricService, AfterLRAListener.class.getName()));

        // verify that the resource was notified of the final state of the LRA
        assertTrue("testAfterLRAListener: end synchronization was not invoked on resource " + resourcePath.getUri(),
                lraMetricService.getMetric(LRAMetricType.Closed, lra, AfterLRAListener.class.getName()) >= 1);
    }

    @Test
    public void leaveLRA() throws WebApplicationException {
        URI lra = lraClient.startLRA(null, lraClientId(), lraTimeout(), ChronoUnit.MILLIS);
        WebTarget resourcePath = tckSuiteTarget.path(LRA_RESOURCE_PATH).path(TRANSACTIONAL_WORK_PATH);
        Response response = resourcePath.request().header(LRA_HTTP_CONTEXT_HEADER, lra).put(Entity.text(""));

        checkStatusAndCloseResponse(Response.Status.OK, response, resourcePath);

        // perform a second request to the same method in the same LRA context to validate that multiple participants are not registered
        resourcePath = tckSuiteTarget.path(LRA_RESOURCE_PATH).path(TRANSACTIONAL_WORK_PATH);
        response = resourcePath.request().header(LRA_HTTP_CONTEXT_HEADER, lra).put(Entity.text(""));
        checkStatusAndCloseResponse(Response.Status.OK, response, resourcePath);

        // call a method annotated with @Leave (should remove the participant from the LRA)
        resourcePath = tckSuiteTarget.path(LRA_RESOURCE_PATH).path("leave");
        response = resourcePath.request().header(LRA_HTTP_CONTEXT_HEADER, lra).put(Entity.text(""));
        checkStatusAndCloseResponse(Response.Status.OK, response, resourcePath);

        lraClient.closeLRA(lra);
        lraTestService.waitForCallbacks(lra);

        // check that participant was not told to complete
        assertEquals("Wrong completion count when participant left the LRA. "
                + "Expecting the completed count hasn't change between start and end of the test. "
                + "The test call went to LRA resource at " + resourcePath.getUri(),
                -1, lraMetricService.getMetric(LRAMetricType.Completed, lra, LraResource.class.getName()));
    }

    @Test
    public void dependentLRA() throws WebApplicationException {
        // call a method annotated with NOT_SUPPORTED but one which programmatically starts an LRA and returns it via a header
        WebTarget resourcePath = tckSuiteTarget.path(LRA_RESOURCE_PATH).path("startViaApi");
        Response response = resourcePath.request().put(Entity.text(""));
        Object lraHeader = response.getHeaders().getFirst(LRA_HTTP_CONTEXT_HEADER);
        String lraId = checkStatusReadAndCloseResponse(Response.Status.OK, response, resourcePath);

        // LRAs started within the invoked remote method should not be available to the caller via the context header
        assertNull("JAX-RS response to PUT request should not have returned the header " + LRA_HTTP_CONTEXT_HEADER
                + ". The test call went to " + resourcePath.getUri(), lraHeader);

        // check that the remote method returned an active LRA (ie check it's not null and then close it)
        assertNotNull("JAX-RS response to PUT request should have returned content of LRA id. The test call went to "
                + resourcePath.getUri(), lraId);

        lraClient.closeLRA(URI.create(lraId));
    }

    @Test
    public void timeLimit() {
        WebTarget resourcePath = tckSuiteTarget.path(LRA_RESOURCE_PATH).path(TIME_LIMIT);
        Response response = resourcePath
                .request()
                .get();

        URI lraId = URI.create(checkStatusReadAndCloseResponse(Response.Status.OK, response, resourcePath));

        // Note that the timeout firing will cause the implementation to compensate
        // the LRA so it may no longer exist
        // (depends upon how long the implementation keeps a record of finished LRAs

        // check that participant was invoked
        lraTestService.waitForCallbacks(lraId);
        
        /*
         * The call to activities/timeLimit should have started an LRA which should have timed out
         * (because the invoked resource method sleeps for longer than the timeLimit annotation
         * attribute specifies). Therefore the participant should have compensated:
         */
        assertEquals("The LRA should have timed out but complete was called instead of compensate. "
                + "Expecting the number of complete call before test matches the ones after LRA timed out. "
                + "The test call went to " + resourcePath.getUri(), 
                0, lraMetricService.getMetric(LRAMetricType.Completed, lraId, LraResource.class.getName()));
        assertEquals("The LRA should have timed out and compensate should be called. "
                + "Expecting the number of compensate call before test is one less lower than the ones after LRA timed out. "
                + "The test call went to " + resourcePath.getUri(), 
                1, lraMetricService.getMetric(LRAMetricType.Compensated, lraId, LraResource.class.getName()));
    }

    /**
     * Service A - Timeout 500 ms
     * Service B - Type.MANDATORY
     *
     * Service A calls Service B after it has waited 1 sec.
     * Service A returns http Status from the call to Service B.
     *
     * test calls A and verifies if return is status 412 (precondition failed)
     * or 410 (gone) since LRA is not active when Service B endpoint is called.
     */
    @Test
    public void timeLimitWithPreConditionFailed() {
        WebTarget resourcePath = tckSuiteTarget.path(LRA_RESOURCE_PATH).path(TIME_LIMIT_HALF_SEC);
        Response response = resourcePath
                .request()
                .get();

        // verify that a 412 response code was generated
        assertTrue("Expected 412 or 410 response",
                response.getStatus() == Response.Status.PRECONDITION_FAILED.getStatusCode() ||
                        response.getStatus() == Response.Status.GONE.getStatusCode()
                );

        response.close();
    }

    @Test
    public void acceptCloseTest() throws WebApplicationException, InterruptedException {
        joinAndEnd(true, LRA_RESOURCE_PATH, ACCEPT_WORK);
    }

    @Test
    public void acceptCancelTest() throws WebApplicationException, InterruptedException {
        joinAndEnd(false, LRA_RESOURCE_PATH, ACCEPT_WORK);
    }

    private void joinAndEnd(boolean close, String path, String path2)
            throws WebApplicationException, InterruptedException {
        URI lra = lraClient.startLRA(null, lraClientId(), lraTimeout(), ChronoUnit.MILLIS);
        WebTarget resourcePath = tckSuiteTarget.path(path).path(path2);

        Response response = resourcePath
                .request().header(LRA_HTTP_CONTEXT_HEADER, lra).put(Entity.text(""));

        checkStatusAndCloseResponse(Response.Status.OK, response, resourcePath);

        if (close) {
            lraClient.closeLRA(lra);
        } else {
            lraClient.cancelLRA(lra);
        }

        lraTestService.waitForRecovery(lra);

        int completionCount = lraMetricService.getMetric(LRAMetricType.Completed, lra, LraResource.class.getName());
        int compensationCount = lraMetricService.getMetric(LRAMetricType.Compensated, lra, LraResource.class.getName());

        // Complete or Compensate methods MUST not be repeated as the resource contains a Status method
        boolean wasCalled = (close ? completionCount == 1 : compensationCount == 1);
        boolean wasNotCalled = (close ? compensationCount == 0 : completionCount == 0);
        String lraMode = (close ? "close" : "cancel");
        String participantMode = (close ? "complete" : "compensate");

        assertTrue(String.format("acceptTest with %s: participant (%s) was not asked to %s (expecting only one call)",
                lraMode, resourcePath.getUri(), participantMode),
                wasCalled);
        assertTrue(String.format("acceptTest with %s: participant (%s) was asked to %s",
                lraMode, resourcePath.getUri(), participantMode),
                wasNotCalled);
        assertTrue("acceptTest: LRA did not finish", lraClient.isLRAFinished(lra));
    }

    @Test
    public void noLRATest() throws WebApplicationException {
        WebTarget resourcePath = tckSuiteTarget
                .path(NoLRAResource.NO_LRA_RESOURCE_PATH)
                .path(NoLRAResource.NON_TRANSACTIONAL_WORK_PATH);

        URI lra = lraClient.startLRA(null, lraClientId(), lraTimeout(), ChronoUnit.MILLIS);

        Response response = resourcePath.request().header(LRA_HTTP_CONTEXT_HEADER, lra)
                .put(Entity.text(""));

        String lraId = checkStatusReadAndCloseResponse(Response.Status.OK, response, resourcePath);

        assertEquals("While calling non-LRA method the resource returns not expected LRA id",
                lraId, lra.toString());

        lraClient.cancelLRA(lra);
        lraTestService.waitForCallbacks(lra);

        // check that second service (the LRA aware one), namely
        // {@link org.eclipse.microprofile.lra.tck.participant.api.LraResource#activityWithMandatoryLRA(String, String)}
        // was told to compensate
        int completedCount = lraMetricService.getMetric(LRAMetricType.Completed, lra, LraResource.class.getName());
        int compensatedCount = lraMetricService.getMetric(LRAMetricType.Compensated, lra, LraResource.class.getName());

        assertEquals("Completed should not be called on the LRA aware service. "
                + "The number of completed count for before and after test does not match. "
                + "The test call went to " + resourcePath.getUri(), 0, completedCount);
        assertEquals("Compensated service should be called on LRA aware service. The number of compensated count after test is bigger for one. "
                + "The test call went to " + resourcePath.getUri(), 1, compensatedCount);
    }

    /**
     * client invokes the same participant method twice in the same LRA context
     * cancel the LRA
     * check that the participant was asked to compensate only once
     */
    @Test
    public void joinWithOneResourceSameMethodTwiceWithCancel() throws WebApplicationException {
        joinWithOneResource("joinWithOneResourceSameMethodTwiceWithCancel",
                false, JOIN_WITH_EXISTING_LRA_PATH, JOIN_WITH_EXISTING_LRA_PATH);
    }

    /**
     * client invokes the same participant twice but using different methods in the same LRA context
     * cancel the LRA
     * check that the participant was asked to compensate only once
     */
    @Test
    public void joinWithOneResourceDifferentMethodTwiceWithCancel() throws WebApplicationException {
        joinWithOneResource("joinWithOneResourceDifferentMethodTwiceWithCancel",
                false, JOIN_WITH_EXISTING_LRA_PATH, JOIN_WITH_EXISTING_LRA_PATH2);
    }

    /**
     * client invokes the same participant method twice in the same LRA context
     * close the LRA
     * check that the participant was asked to complete only once
     */
    @Test
    public void joinWithOneResourceSameMethodTwiceWithClose() throws WebApplicationException {
        joinWithOneResource("joinWithOneResourceSameMethodTwiceWithClose",
                true, JOIN_WITH_EXISTING_LRA_PATH, JOIN_WITH_EXISTING_LRA_PATH);
    }

    /**
     * client invokes the same participant twice but using different methods in the same LRA context
     * close the LRA
     * check that the participant was asked to complete only once
     */
    @Test
    public void joinWithOneResourceDifferentMethodTwiceWithClose() throws WebApplicationException {
        joinWithOneResource("joinWithOneResourceDifferentMethodTwiceWithClose",
                true, JOIN_WITH_EXISTING_LRA_PATH, JOIN_WITH_EXISTING_LRA_PATH2);
    }

    /**
     * client invokes different participant in the same LRA context
     * close the LRA
     * check that both participants were asked to complete
     */
    @Test
    public void joinWithTwoResourcesWithClose() throws WebApplicationException {
        joinWithTwoResources(true);
    }

    /**
     * client invokes different participants in the same LRA context
     * cancel the LRA
     * check that both participants were asked to compensate
     */
    @Test
    public void joinWithTwoResourcesWithCancel() throws WebApplicationException {
        joinWithTwoResources(false);
    }

    private void joinWithOneResource(String methodName, boolean close, String resource1Method, String resource2Method)
            throws WebApplicationException {
        // set up the web target for the test
        WebTarget resource1Path = tckSuiteTarget.path(TCK_PARTICIPANT_RESOURCE_PATH).path(resource1Method);
        WebTarget resource2Path = tckSuiteTarget.path(TCK_PARTICIPANT_RESOURCE_PATH).path(resource2Method);

        URI lra = lraClient.startLRA(null, lraClientId(), lraTimeout(), ChronoUnit.MILLIS);

        // invoke the same JAX-RS resources twice in the context of the lra which should enlist the resource only once:
        Response response1 = resource1Path.request().header(LRA_HTTP_CONTEXT_HEADER, lra).put(Entity.text(""));
        checkStatusAndCloseResponse(Response.Status.OK, response1, resource1Path);
        Response response2 = resource2Path.request().header(LRA_HTTP_CONTEXT_HEADER, lra).put(Entity.text(""));
        checkStatusAndCloseResponse(Response.Status.OK, response2, resource2Path);

        if (close) {
            lraClient.closeLRA(lra);
            lraTestService.waitForCallbacks(lra);
            
            assertTrue(methodName + ": resource should have completed with no compensations",
                    1 <= lraMetricService.getMetric(LRAMetricType.Completed, lra, ParticipatingTckResource.class.getName()) &&
                    0 == lraMetricService.getMetric(LRAMetricType.Compensated, lra, ParticipatingTckResource.class.getName()));
        } else {
            lraClient.cancelLRA(lra);
            lraTestService.waitForCallbacks(lra);

            assertTrue(methodName + ":: resource should have compensated with no completions",
                    0 == lraMetricService.getMetric(LRAMetricType.Completed, lra, ParticipatingTckResource.class.getName()) &&
                    1 <= lraMetricService.getMetric(LRAMetricType.Compensated, lra, ParticipatingTckResource.class.getName()));
        }
    }

    private void joinWithTwoResources(boolean close) throws WebApplicationException {
        // set up the web target for the test
        WebTarget resource1Path = tckSuiteTarget.path(LRA_RESOURCE_PATH).path(TRANSACTIONAL_WORK_PATH);
        WebTarget resource2Path = tckSuiteTarget.path(TCK_PARTICIPANT_RESOURCE_PATH).path(JOIN_WITH_EXISTING_LRA_PATH);

        URI lra = lraClient.startLRA(null, lraClientId(), lraTimeout(), ChronoUnit.MILLIS);

        // invoke two JAX-RS resources in the context of the lra which should enlist them both:
        Response response1 = resource1Path.request().header(LRA_HTTP_CONTEXT_HEADER, lra).put(Entity.text(""));
        checkStatusAndCloseResponse(Response.Status.OK, response1, resource1Path);
        Response response2 = resource2Path.request().header(LRA_HTTP_CONTEXT_HEADER, lra).put(Entity.text(""));
        checkStatusAndCloseResponse(Response.Status.OK, response2, resource2Path);

        if (close) {
            lraClient.closeLRA(lra);
            lraTestService.waitForCallbacks(lra);

            assertTrue("joinWithTwoResourcesWithClose: both resources should have completed",
                    lraMetricService.getMetric(LRAMetricType.Completed, lra, LraResource.class.getName()) == 1 &&
                    lraMetricService.getMetric(LRAMetricType.Completed, lra, ParticipatingTckResource.class.getName()) >= 1);
        } else {
            lraClient.cancelLRA(lra);
            lraTestService.waitForCallbacks(lra);

            assertTrue("joinWithTwoResourcesWithCancel: both resources should have compensated",
                    lraMetricService.getMetric(LRAMetricType.Compensated, lra, LraResource.class.getName()) == 1 &&
                    lraMetricService.getMetric(LRAMetricType.Compensated, lra, ParticipatingTckResource.class.getName()) >= 1);
        }
    }

    private void multiLevelNestedActivity(CompletionType how, int nestedCnt) throws WebApplicationException {
        WebTarget resourcePath = tckSuiteTarget.path(LRA_RESOURCE_PATH).path("multiLevelNestedActivity");

        if (how == CompletionType.mixed && nestedCnt <= 1) {
            how = CompletionType.complete;
        }

        URI lra = lraClient.startLRA(null, lraClientId(), lraTimeout(), ChronoUnit.MILLIS);
        String lraId = lra.toString();

        Response response = resourcePath
                .queryParam("nestedCnt", nestedCnt)
                .request()
                .header(LRA_HTTP_CONTEXT_HEADER, lra)
                .put(Entity.text(""));

        String lraStr = checkStatusReadAndCloseResponse(Response.Status.OK, response, resourcePath);
        assertNotNull("expecting a LRA string returned from " + resourcePath.getUri(), lraStr);
        String[] lraArray = lraStr.split(","); // We keep here type String (and not URI) because of the easy String.split
        URI[] uris = new URI[lraArray.length];

        IntStream.range(0, uris.length).forEach(i -> {
            try {
                uris[i] = new URI(lraArray[i]);
            } catch (URISyntaxException e) {
                fail(String.format("%s (multiLevelNestedActivity): returned an invalid URI: %s",
                        resourcePath.getUri().toString(), e.getMessage()));
            }
        });
        // check that the multiLevelNestedActivity method returned the mandatory LRA followed by any nested LRAs
        assertEquals("multiLevelNestedActivity: step 1 (the test call went to " + resourcePath.getUri() + ")",
                nestedCnt + 1, lraArray.length);
        // first element should be the mandatory LRA
        assertEquals("multiLevelNestedActivity: step 2 (the test call went to " + resourcePath.getUri() + ")",
                lraId, lraArray[0]);

        // and the mandatory lra seen by the multiLevelNestedActivity method
        assertFalse("multiLevelNestedActivity: top level LRA should be active (path called " + resourcePath.getUri() + ")",
                lraClient.isLRAFinished(URI.create( lraArray[0])));

        lraTestService.waitForCallbacks(lra);
        int inMiddleCompletedCount = lraMetricService.getMetric(LRAMetricType.Completed);
        int inMiddleCompensatedCount = lraMetricService.getMetric(LRAMetricType.Compensated);

        // check that all nested activities were told to complete
        assertEquals("multiLevelNestedActivity: step 3 (called test path " + resourcePath.getUri() + ")",
                nestedCnt, inMiddleCompletedCount);
        // and that neither were told to compensate
        assertEquals("multiLevelNestedActivity: step 4 (called test path " + resourcePath.getUri() + ")",
                0, inMiddleCompensatedCount);

        // close the LRA
        if (how == CompletionType.compensate) {
            lraClient.cancelLRA(lra);
        } else if (how == CompletionType.complete) {
            lraClient.closeLRA(lra);
        } else {
            /*
             * The test is calling for a mixed outcome (a top level LRA L1 and nestedCnt nested LRAs (L2, L3, ...)::
             * L1 the mandatory call (PUT "lraresource/multiLevelNestedActivity") registers participant C1
             * the resource makes nestedCnt calls to "lraresource/nestedActivity" each of which create nested LRAs
             * L2, L3, ... each of which enlists a participant (C2, C3, ...) which are completed when the call returns
             * L2 is cancelled which causes C2 to compensate
             * L1 is closed which triggers the completion of C1
             *
             * To summarise:
             *
             * - C1 is completed
             * - C2 is completed and then compensated
             * - C3, ... are completed
             */

            // compensate the first nested LRA in the enlisted resource
            tckSuiteTarget
                .path(LRA_RESOURCE_PATH).path(CANCEL_PATH)
                .request()
                .header(LRA_HTTP_CONTEXT_HEADER, uris[1])
                .put(Entity.text(""));

            lraClient.closeLRA(lra); // should not complete any nested LRAs (since they have already completed via the interceptor)
        }

        // validate that the top level and nested LRAs are gone
        IntStream.rangeClosed(0, nestedCnt).forEach(i -> assertTrue(
                String.format("multiLevelNestedActivity: %s LRA still active (resource path was %s)",
                        (i == 0 ? "top level" : "nested"), resourcePath.getUri()),
                lraClient.isLRAFinished(URI.create(lraArray[i]))));

        lraTestService.waitForCallbacks(lra);
        int afterCompletedCount = lraMetricService.getMetric(LRAMetricType.Completed);
        int afterCompensatedCount = lraMetricService.getMetric(LRAMetricType.Compensated);

        if (how == CompletionType.complete) {
            // make sure that all nested activities were not told to complete or cancel a second time
            assertEquals("multiLevelNestedActivity: step 5 (called test path " + resourcePath.getUri() + ")",
                    inMiddleCompletedCount + nestedCnt, afterCompletedCount);
            // and that neither were still not told to compensate
            assertEquals("multiLevelNestedActivity: step 6 (called test path " + resourcePath.getUri() + ")",
                    0, afterCompensatedCount);

        } else if (how == CompletionType.compensate) {
            /*
             * the test starts LRA1 calls a @Mandatory method multiLevelNestedActivity which enlists in LRA1
             * multiLevelNestedActivity then calls an @Nested method which starts L2 and enlists another participant
             *   when the method returns the nested participant is completed (ie completed count is incremented)
             * Canceling L1 should then compensate the L1 enlistment (ie compensate count is incremented)
             * which will then tell L2 to compensate (ie the compensate count is incremented again)
             */
            // each nested participant should have completed (the +nestedCnt)
            assertEquals("multiLevelNestedActivity: step 7 (called test path " + resourcePath.getUri() + ")",
                    nestedCnt, afterCompletedCount);
            // each nested participant should have compensated. The top level enlistment should have compensated (the +1)
            assertEquals("multiLevelNestedActivity: step 8 (called test path " + resourcePath.getUri() + ")",
                    inMiddleCompensatedCount + 1 + nestedCnt, afterCompensatedCount);
        } else {
            /*
             * The test is calling for a mixed outcome:
             * - the top level LRA was closed
             * - one of the nested LRAs was compensated the rest should have been completed
             */
            // there should be just 1 compensation (the first nested LRA)
            assertEquals("multiLevelNestedActivity: step 9 (called test path " + resourcePath.getUri() + ")",
                    1, afterCompensatedCount);
            /*
             * Expect nestedCnt + 1 completions, 1 for the top level and one for each nested LRA
             * (NB the first nested LRA is completed and compensated)
             * Note that the top level complete should not call complete again on the nested LRA
             */
            assertEquals("multiLevelNestedActivity: step 10 (called test path " + resourcePath.getUri() + ")",
                    nestedCnt + 1, afterCompletedCount);
        }
    }
}