package com.atlassian.bitbucket.mesh.git.hook.script;

import com.atlassian.bitbucket.mesh.AbstractWiredExecutionContextTest;
import com.atlassian.bitbucket.mesh.git.ref.RefUpdate;
import com.atlassian.bitbucket.mesh.layout.StorageLayout;
import com.atlassian.bitbucket.mesh.process.ProcessSummary;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import org.apache.commons.lang3.mutable.MutableInt;
import org.hamcrest.MatcherAssert;
import org.hamcrest.collection.IsIterableContainingInOrder;
import org.junit.Test;

import javax.annotation.Nonnull;
import javax.annotation.Resource;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

import static java.util.Collections.singletonList;

public class DefaultHookScriptManagerTest extends AbstractWiredExecutionContextTest {

    private static final String ID_1 = "hook-scripts/1/1";
    private static final String ID_2 = "hook-scripts/2/1";
    private static final RefUpdate REF_UPDATE = RefUpdate.of("refs/heads/branch_mod_merge",
            "042fb0cc198e7320b59a025689b45a2cb52f4684", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad");
    private static final String SCRIPT_FAST = "echo \"Ran script\"\n";
    private static final String SCRIPT_FULL = "#!/usr/bin/env perl\n" +
            "my $input = <>;\n" +
            "chomp($input);\n" +
            "if ($input =~ /([A-Fa-f0-9]{40}) ([A-Fa-f0-9]{40}) (.+)$/) {\n" +
            "  print \"Updating $3 from $1 to $2\\n\";\n" + //Some stdout for us to verify
            "\n" +
            "  my $output = `git rev-list --count ^$1 $2 2>&1`;\n" + //A git process to verify the sandbox
            "  my $exitCode = $?;\n" +
            "  chomp($output);\n" +
            "  if ($exitCode) {\n" +
            "    # This is unexpected; the rev-list should have succeeded\n" +
            "    print STDERR \"git rev-list failed ($exitCode): $output\\n\";\n" +
            "    exit 1;\n" +
            "  }\n" +
            "  print STDERR \"Added $output commit(s)\\n\";\n" + //Some stderr for us to verify
            "}\n";

    @Resource
    private HookScriptManager manager;
    @Repo
    private Repository repository;
    @Resource
    private StorageLayout storageLayout;

    @Test
    public void testCall() {
        writeScript(ID_1, SCRIPT_FULL);

        MutableInt exitCode = new MutableInt(-1);
        AbstractHookScriptCallback callback = new AbstractHookScriptCallback() {

            @Override
            public void onComplete(@Nonnull ProcessSummary summary) {
                exitCode.setValue(summary.getExitCode());
            }
        };

        manager.call(singletonList(ID_1),
                new CallHookScriptParameters.Builder(repository)
                        .executionTimeout(Duration.ofMinutes(2L))
                        .refUpdates(singletonList(REF_UPDATE))
                        .build(),
                callback);

        //Test stderr first so that, if the rev-list fails, we see its output in the mismatch
        assertEquals("Added 1 commit(s)\n", callback.getStderr());
        //Test stdout next so we can verify the RefUpdate roundtripped the script correctly
        assertEquals("Updating refs/heads/branch_mod_merge from 042fb0cc198e7320b59a025689b45a2cb52f4684 " +
                "to 6053a1eaa1c009dd11092d09a72f3c41af1b59ad\n", callback.getStdout());
        //Test the exit code last, because there's not much chance of a failure at this point
        assertEquals(0, exitCode.intValue());
    }

    @Test
    public void testCallStopsAfterCancel() {
        writeScript(ID_1, SCRIPT_FAST);
        writeScript(ID_2, SCRIPT_FAST);

        List<String> scripts = new ArrayList<>();
        AbstractHookScriptCallback callback = new AbstractHookScriptCallback() {

            private int count;

            @Override
            public boolean isCanceled() {
                return count++ > 0;
            }

            @Override
            public void onStartScript(@Nonnull String hookScriptId) {
                scripts.add(hookScriptId);
            }
        };

        manager.call(singletonList(ID_1),
                new CallHookScriptParameters.Builder(repository)
                        .executionTimeout(Duration.ofMinutes(2L))
                        .refUpdates(singletonList(REF_UPDATE))
                        .build(),
                callback);

        MatcherAssert.assertThat(scripts, IsIterableContainingInOrder.contains(ID_1)); //ID_2 should not have started
        assertEquals("Ran script\n", callback.getStdout());
        assertEquals("", callback.getStderr());
    }

    @Test
    public void testCallStopsAfterVeto() {
        //The first script has a non-zero exit, so the second script should not run
        writeScript(ID_1, SCRIPT_FAST + "exit 1\n");
        writeScript(ID_2, SCRIPT_FAST);

        List<String> scripts = new ArrayList<>();
        AbstractHookScriptCallback callback = new AbstractHookScriptCallback() {

            @Override
            public void onStartScript(@Nonnull String hookScriptId) {
                scripts.add(hookScriptId);
            }
        };

        manager.call(singletonList(ID_1),
                new CallHookScriptParameters.Builder(repository)
                        .abortOnFirstVeto(true)
                        .executionTimeout(Duration.ofMinutes(2L))
                        .refUpdates(singletonList(REF_UPDATE))
                        .build(),
                callback);

        MatcherAssert.assertThat(scripts, IsIterableContainingInOrder.contains(ID_1)); //ID_2 should not have started
        assertEquals("Ran script\n", callback.getStdout());
        assertEquals("", callback.getStderr());
    }

    private void writeScript(String id, String script) {
        manager.put(id, stream -> stream.write(script.getBytes(StandardCharsets.UTF_8)));
        assertTrue(manager.exists(id));
    }

    private abstract static class AbstractHookScriptCallback implements HookScriptCallback {

        private final StringBuilder stderr;
        private final StringBuilder stdout;

        AbstractHookScriptCallback() {
            stderr = new StringBuilder();
            stdout = new StringBuilder();
        }

        @Override
        public boolean isCanceled() {
            return false;
        }

        @Override
        public void onStartScript(@Nonnull String hookScriptId) {

        }

        @Override
        public void onComplete(@Nonnull ProcessSummary summary) {
        }

        @Override
        public void onStderr(@Nonnull ByteBuffer buffer, boolean closed) {
            if (buffer.hasRemaining()) {
                stderr.append(StandardCharsets.UTF_8.decode(buffer));
            }
        }

        @Override
        public void onStdout(@Nonnull ByteBuffer buffer, boolean closed) {
            if (buffer.hasRemaining()) {
                stdout.append(StandardCharsets.UTF_8.decode(buffer));
            }
        }

        String getStderr() {
            return stderr.toString();
        }

        String getStdout() {
            return stdout.toString();
        }
    }
}
