package com.nirima.jenkins.plugins.docker;

import java.io.IOException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import org.slf4j.Logger;

import com.github.dockerjava.api.model.Container;

import hudson.model.Node;
import io.jenkins.docker.DockerTransientNode;
import io.jenkins.docker.client.DockerAPI;

public class DockerContainerWatchdogTest {
    @Test
    public void testEmptyEnvironment() throws IOException, InterruptedException {
        TestableDockerContainerWatchdog subject = new TestableDockerContainerWatchdog();

        subject.setAllNodes(new LinkedList<>());
        subject.setAllClouds(new LinkedList<>());

        subject.runExecute();

        Assert.assertEquals(0, subject.getAllRemovedNodes().size());
    }

    @Test
    public void testSimpleEnvironmentNothingTodo() throws IOException, InterruptedException {
        TestableDockerContainerWatchdog subject = new TestableDockerContainerWatchdog();

        final String nodeName = "unittest-12345";
        final String containerId = UUID.randomUUID().toString();

        /* setup of cloud */
        List<DockerCloud> listOfCloud = new LinkedList<>();

        Map<String, String> labelMap = new HashMap<>();
        labelMap.put(DockerContainerLabelKeys.NODE_NAME, nodeName);
        labelMap.put(DockerContainerLabelKeys.REMOVE_VOLUMES, "false");

        List<Container> containerList = new LinkedList<>();
        Container c = TestableDockerContainerWatchdog.createMockedContainer(containerId, "Running", 0L, labelMap);
        containerList.add(c);

        DockerAPI dockerApi = TestableDockerContainerWatchdog.createMockedDockerAPI(containerList);
        DockerCloud cloud = new DockerCloud("unittestcloud", dockerApi, new LinkedList<>());
        listOfCloud.add(cloud);

        subject.setAllClouds(listOfCloud);

        /* setup of nodes */
        LinkedList<Node> allNodes = new LinkedList<>();

        DockerTransientNode node = TestableDockerContainerWatchdog.createMockedDockerTransientNode(containerId, nodeName, cloud, false);
        allNodes.add(node);

        subject.setAllNodes(allNodes);

        subject.runExecute();

        Assert.assertEquals(0, subject.getAllRemovedNodes().size());
    }

    @Test
    public void testContainerExistsButAgentIsMissing() throws IOException, InterruptedException {
        TestableDockerContainerWatchdog subject = new TestableDockerContainerWatchdog();

        final String nodeName = "unittest-12345";
        final String containerId = UUID.randomUUID().toString();

        /* setup of cloud */
        List<DockerCloud> listOfCloud = new LinkedList<>();

        Map<String, String> labelMap = new HashMap<>();
        labelMap.put(DockerContainerLabelKeys.NODE_NAME, nodeName);
        labelMap.put(DockerContainerLabelKeys.TEMPLATE_NAME, "unittesttemplate");
        labelMap.put(DockerContainerLabelKeys.REMOVE_VOLUMES, "false");

        List<Container> containerList = new LinkedList<>();
        Container c = TestableDockerContainerWatchdog.createMockedContainer(containerId, "Running", 0L, labelMap);
        containerList.add(c);

        DockerAPI dockerApi = TestableDockerContainerWatchdog.createMockedDockerAPI(containerList);
        DockerCloud cloud = new DockerCloud("unittestcloud", dockerApi, new LinkedList<>());
        listOfCloud.add(cloud);

        subject.setAllClouds(listOfCloud);

        /* setup of nodes */
        LinkedList<Node> allNodes = new LinkedList<>();
        subject.setAllNodes(allNodes);

        subject.runExecute();

        Assert.assertEquals(0, subject.getAllRemovedNodes().size());

        List<String> containersRemoved = subject.getContainersRemoved();
        Assert.assertEquals(1, containersRemoved.size());
        Assert.assertEquals(containerId, containersRemoved.get(0));
    }

    @Test
    public void testContainerExistsButAgentIsMissingWrongNodeNameIsIgnored() throws IOException, InterruptedException {
        TestableDockerContainerWatchdog subject = new TestableDockerContainerWatchdog();

        final String nodeName = "unittest-12345";
        final String containerId = UUID.randomUUID().toString();

        /* setup of cloud */
        List<DockerCloud> listOfCloud = new LinkedList<>();

        Map<String, String> labelMap = new HashMap<>();
        labelMap.put(DockerContainerLabelKeys.NODE_NAME, nodeName);
        labelMap.put(DockerContainerLabelKeys.TEMPLATE_NAME, "unittesttemplate");
        labelMap.put(DockerContainerLabelKeys.REMOVE_VOLUMES, "false");

        List<Container> containerList = new LinkedList<>();
        Container c = TestableDockerContainerWatchdog.createMockedContainer(containerId, "Running", 0L, labelMap);
        containerList.add(c);

        DockerAPI dockerApi = TestableDockerContainerWatchdog.createMockedDockerAPI(containerList);
        DockerCloud cloud = new DockerCloud("unittestcloud", dockerApi, new LinkedList<>());
        listOfCloud.add(cloud);

        subject.setAllClouds(listOfCloud);

        /* setup of nodes */
        LinkedList<Node> allNodes = new LinkedList<>();

        Node n = TestableDockerContainerWatchdog.createMockedDockerTransientNode(UUID.randomUUID().toString(), "unittest-other", cloud, false);
        allNodes.add(n);

        subject.setAllNodes(allNodes);

        subject.runExecute();

        List<String> containersRemoved = subject.getContainersRemoved();
        Assert.assertEquals(1, containersRemoved.size());
        Assert.assertEquals(containerId, containersRemoved.get(0));

        Assert.assertEquals(0, subject.getAllRemovedNodes().size());
    }

    @Test
    public void testContainerExistsButAgentIsMissingTwoClouds() throws IOException, InterruptedException {
        TestableDockerContainerWatchdog subject = new TestableDockerContainerWatchdog();

        final String nodeName1 = "unittest-12345";
        final String containerId1 = "12345f63-cce3-4188-82dc-451b444daa40";

        final String nodeName2 = "unittest-12346";
        final String containerId2 = "12346f63-cce3-4188-82dc-451b444daa40";

        /* setup of cloud */
        List<DockerCloud> listOfCloud = new LinkedList<>();

        List<Container> containerList = new LinkedList<>();

        Map<String, String> labelMap = new HashMap<>();
        labelMap.put(DockerContainerLabelKeys.NODE_NAME, nodeName1);
        labelMap.put(DockerContainerLabelKeys.TEMPLATE_NAME, "unittestTemplate");
        labelMap.put(DockerContainerLabelKeys.REMOVE_VOLUMES, "false");

        Container c = TestableDockerContainerWatchdog.createMockedContainer(containerId1, "Running", 0L, labelMap);
        containerList.add(c);

        labelMap = new HashMap<>();
        labelMap.put(DockerContainerLabelKeys.NODE_NAME, nodeName2);
        labelMap.put(DockerContainerLabelKeys.TEMPLATE_NAME, "unittestTemplate");
        labelMap.put(DockerContainerLabelKeys.REMOVE_VOLUMES, "false");

        c = TestableDockerContainerWatchdog.createMockedContainer(containerId2, "Running", 0L, labelMap);
        containerList.add(c);

        DockerAPI dockerApi = TestableDockerContainerWatchdog.createMockedDockerAPI(containerList);
        DockerCloud cloud1 = new DockerCloud("unittestcloud1", dockerApi, new LinkedList<>());
        listOfCloud.add(cloud1);

        DockerCloud cloud2 = new DockerCloud("unittestcloud2", dockerApi, new LinkedList<>());
        listOfCloud.add(cloud2);

        subject.setAllClouds(listOfCloud);

        /* setup of nodes */
        LinkedList<Node> allNodes = new LinkedList<>();
        subject.setAllNodes(allNodes);

        subject.runExecute();

        List<String> containersRemoved = subject.getContainersRemoved();
        Assert.assertEquals(4, containersRemoved.size());

        int countContainer1 = 0;
        int countContainer2 = 0;
        for (String containerId : containersRemoved) {
            if (containerId.equals(containerId1)) {
                countContainer1++;
            } else if (containerId.equals(containerId2)) {
                countContainer2++;
            } else {
                Assert.fail("Unknown container identifier");
            }
        }

        Assert.assertEquals(2, countContainer1);
        Assert.assertEquals(2, countContainer2);

        /* NB: Why 2 here?
         * keep in mind that the same containers are associated with the same DockerClient.
         * Thus, the same containers also appear twice to our subject - and thus will send the termination
         * requests twice.
         * 
         * Note that this is an acceptable real-life behavior, which is uncommon, though.
         */

        Assert.assertEquals(0, subject.getAllRemovedNodes().size());
    }

    @Test
    public void testContainerExistsButAgentIsMissingWithTemplate() throws IOException, InterruptedException {
        TestableDockerContainerWatchdog subject = new TestableDockerContainerWatchdog();

        final String nodeName = "unittest-12345";
        final String containerId = UUID.randomUUID().toString();

        /* setup of cloud */
        List<DockerCloud> listOfCloud = new LinkedList<>();

        Map<String, String> labelMap = new HashMap<>();
        labelMap.put(DockerContainerLabelKeys.NODE_NAME, nodeName);
        labelMap.put(DockerContainerLabelKeys.TEMPLATE_NAME, "unittesttemplate");
        labelMap.put(DockerContainerLabelKeys.REMOVE_VOLUMES, "false");

        List<Container> containerList = new LinkedList<>();
        Container c = TestableDockerContainerWatchdog.createMockedContainer(containerId, "Running", 0L, labelMap);
        containerList.add(c);

        DockerAPI dockerApi = TestableDockerContainerWatchdog.createMockedDockerAPI(containerList);
        DockerCloud cloud = new DockerCloud("unittestcloud", dockerApi, new LinkedList<>());
        listOfCloud.add(cloud);

        DockerTemplate template = Mockito.mock(DockerTemplate.class);
        Mockito.when(template.getName()).thenReturn("unittesttemplate");
        Mockito.when(template.isRemoveVolumes()).thenReturn(true);
        cloud.addTemplate(template);

        subject.setAllClouds(listOfCloud);

        /* setup of nodes */
        LinkedList<Node> allNodes = new LinkedList<>();
        subject.setAllNodes(allNodes);

        subject.runExecute();

        Mockito.verify(dockerApi.getClient(), Mockito.times(0)).removeContainerCmd(containerId); // enforced termination shall not happen

        Assert.assertEquals(0, subject.getAllRemovedNodes().size());

        List<String> containersRemoved = subject.getContainersRemoved();
        Assert.assertEquals(1, containersRemoved.size());

        Assert.assertEquals(containerId, containersRemoved.get(0));
    }

    @Test
    public void testContainerExistsButAgentIsMissingTooEarly() throws IOException, InterruptedException {
        TestableDockerContainerWatchdog subject = new TestableDockerContainerWatchdog();

        Clock clock = Clock.fixed(Instant.ofEpochMilli(1527970544000L), ZoneId.of("UTC"));
        subject.setClock(clock);

        final String nodeName = "unittest-12345";
        final String containerId = UUID.randomUUID().toString();

        /* setup of cloud */
        List<DockerCloud> listOfCloud = new LinkedList<>();

        Map<String, String> labelMap = new HashMap<>();
        labelMap.put(DockerContainerLabelKeys.NODE_NAME, nodeName);
        labelMap.put(DockerContainerLabelKeys.TEMPLATE_NAME, "unittesttemplate");
        labelMap.put(DockerContainerLabelKeys.REMOVE_VOLUMES, "false");

        List<Container> containerList = new LinkedList<>();
        Container c = TestableDockerContainerWatchdog.createMockedContainer(containerId, "Running", clock.instant().toEpochMilli() / 1000, labelMap);
        containerList.add(c);

        DockerAPI dockerApi = TestableDockerContainerWatchdog.createMockedDockerAPI(containerList);
        DockerCloud cloud = new DockerCloud("unittestcloud", dockerApi, new LinkedList<>());
        listOfCloud.add(cloud);

        subject.setAllClouds(listOfCloud);

        /* setup of nodes */
        LinkedList<Node> allNodes = new LinkedList<>();
        subject.setAllNodes(allNodes);

        subject.runExecute();

        // shall not have called to remove it by force
        Mockito.verify(dockerApi.getClient(), Mockito.times(0)).removeContainerCmd(containerId);

        // ... and shall not have called to remove it gracefully
        Assert.assertEquals(0, subject.getContainersRemoved().size());

        // but, if we turn back time a little... 
        subject.setClock(Clock.offset(clock, Duration.ofMinutes(5)));

        subject.runExecute();

        // ... then it should work
        List<String> containersRemoved = subject.getContainersRemoved();
        Assert.assertEquals(1, containersRemoved.size());
        Assert.assertEquals(containerId, containersRemoved.get(0));
    }

    @Test
    public void testContainerExistsButAgentIsMissingRemoveVolumesLabelMissing() throws IOException, InterruptedException {
        TestableDockerContainerWatchdog subject = new TestableDockerContainerWatchdog();

        final String nodeName = "unittest-12345";
        final String containerId = UUID.randomUUID().toString();

        /* setup of cloud */
        List<DockerCloud> listOfCloud = new LinkedList<>();

        Map<String, String> labelMap = new HashMap<>();
        labelMap.put(DockerContainerLabelKeys.NODE_NAME, nodeName);
        labelMap.put(DockerContainerLabelKeys.TEMPLATE_NAME, "unittesttemplate");
        // NB The removeVolumes label is not set here

        List<Container> containerList = new LinkedList<>();
        Container c = TestableDockerContainerWatchdog.createMockedContainer(containerId, "Running", 0L, labelMap);
        containerList.add(c);

        DockerAPI dockerApi = TestableDockerContainerWatchdog.createMockedDockerAPI(containerList);
        DockerCloud cloud = new DockerCloud("unittestcloud", dockerApi, new LinkedList<>());
        listOfCloud.add(cloud);

        subject.setAllClouds(listOfCloud);

        /* setup of nodes */
        LinkedList<Node> allNodes = new LinkedList<>();

        Node n = TestableDockerContainerWatchdog.createMockedDockerTransientNode(UUID.randomUUID().toString(), nodeName, cloud, false);
        allNodes.add(n);

        subject.setAllNodes(allNodes);

        subject.runExecute();

        List<String> containersRemoved = subject.getContainersRemoved();
        Assert.assertEquals(0, containersRemoved.size());

        Assert.assertEquals(0, subject.getAllRemovedNodes().size());
    }

    private static class RemovalTweakingTestableDockerContainerWatchdog extends TestableDockerContainerWatchdog {
        private Clock currentTestClock;

        public RemovalTweakingTestableDockerContainerWatchdog(Clock currentTestClock) {
            super();
            this.currentTestClock = currentTestClock;
        }

        @Override
        protected void removeNode(DockerTransientNode dtn) throws IOException {
            super.removeNode(dtn);

            currentTestClock = Clock.offset(currentTestClock, Duration.ofMinutes(10));
            this.setClock(currentTestClock);
        }

        @Override
        protected boolean stopAndRemoveContainer(DockerAPI dockerApi, Logger logger, String description,
                boolean removeVolumes, String containerId, boolean stop) {
            boolean result = super.stopAndRemoveContainer(dockerApi, logger, description, removeVolumes, containerId, stop);

            currentTestClock = Clock.offset(currentTestClock, Duration.ofMinutes(10));
            this.setClock(currentTestClock);

            return result;
        }
    }

    @Test
    public void testContainersExistsSlowRemovalShallNotOverspill() throws IOException, InterruptedException {
        Clock clock = Clock.fixed(Instant.ofEpochMilli(1527970544000L), ZoneId.of("UTC"));

        TestableDockerContainerWatchdog subject = new RemovalTweakingTestableDockerContainerWatchdog(clock);
        subject.setClock(clock);


        final String nodeName = "unittest-12345";
        final String containerId1 = UUID.randomUUID().toString();
        final String containerId2 = UUID.randomUUID().toString();

        /* setup of cloud */
        List<DockerCloud> listOfCloud = new LinkedList<>();

        Map<String, String> labelMap = new HashMap<>();
        labelMap.put(DockerContainerLabelKeys.NODE_NAME, nodeName);
        labelMap.put(DockerContainerLabelKeys.TEMPLATE_NAME, "unittesttemplate");
        labelMap.put(DockerContainerLabelKeys.REMOVE_VOLUMES, "false");

        List<Container> containerList = new LinkedList<>();
        Container c1 = TestableDockerContainerWatchdog.createMockedContainer(containerId1, "Running", clock.instant().toEpochMilli() / 1000, labelMap);
        containerList.add(c1);

        Container c2 = TestableDockerContainerWatchdog.createMockedContainer(containerId2, "Running", clock.instant().toEpochMilli() / 1000, labelMap);
        containerList.add(c2);

        DockerAPI dockerApi = TestableDockerContainerWatchdog.createMockedDockerAPI(containerList);
        DockerCloud cloud = new DockerCloud("unittestcloud", dockerApi, new LinkedList<>());
        listOfCloud.add(cloud);

        subject.setAllClouds(listOfCloud);

        /* setup of nodes */
        LinkedList<Node> allNodes = new LinkedList<>();
        subject.setAllNodes(allNodes);

        subject.runExecute();

        // shall not have called to remove it by force
        Mockito.verify(dockerApi.getClient(), Mockito.times(0)).removeContainerCmd(containerId1);

        // ... and shall not have tried to remove neither the first nor the second
        Assert.assertEquals(0, subject.getContainersRemoved().size());
    }


    @Test
    public void testAgentExistsButNoContainer() throws IOException, InterruptedException {
        TestableDockerContainerWatchdog subject = new TestableDockerContainerWatchdog();

        final String nodeName = "unittest-78901";
        final String containerId = UUID.randomUUID().toString();

        /* setup of cloud */
        List<DockerCloud> listOfCloud = new LinkedList<>();

        List<Container> containerList = new LinkedList<>();

        DockerAPI dockerApi = TestableDockerContainerWatchdog.createMockedDockerAPI(containerList);
        DockerCloud cloud = new DockerCloud("unittestcloud", dockerApi, new LinkedList<>());
        listOfCloud.add(cloud);

        subject.setAllClouds(listOfCloud);

        /* setup of nodes */
        LinkedList<Node> allNodes = new LinkedList<>();

        DockerTransientNode node = TestableDockerContainerWatchdog.createMockedDockerTransientNode(containerId, nodeName, cloud, true);
        allNodes.add(node);

        subject.setAllNodes(allNodes);

        subject.runExecute();

        List<DockerTransientNode> nodes = subject.getAllRemovedNodes();
        Assert.assertEquals(1, nodes.size());

        DockerTransientNode removedNode = nodes.get(0);
        Assert.assertEquals(node, removedNode);
    }

    @Test
    public void testAgentExistsButNoContainerCheckForTimeout() throws IOException, InterruptedException {
        Clock clock = Clock.fixed(Instant.ofEpochMilli(1527970544000L), ZoneId.of("UTC"));
        TestableDockerContainerWatchdog subject = new RemovalTweakingTestableDockerContainerWatchdog(clock);
        subject.setClock(clock);

        final String nodeName1 = "unittest-78901";
        final String nodeName2 = "unittest-78902";
        final String containerId = UUID.randomUUID().toString();

        /* setup of cloud */
        List<DockerCloud> listOfCloud = new LinkedList<>();

        List<Container> containerList = new LinkedList<>();

        DockerAPI dockerApi = TestableDockerContainerWatchdog.createMockedDockerAPI(containerList);
        DockerCloud cloud = new DockerCloud("unittestcloud", dockerApi, new LinkedList<>());
        listOfCloud.add(cloud);

        subject.setAllClouds(listOfCloud);

        /* setup of nodes */
        LinkedList<Node> allNodes = new LinkedList<>();

        DockerTransientNode node1 = TestableDockerContainerWatchdog.createMockedDockerTransientNode(containerId, nodeName1, cloud, true);
        allNodes.add(node1);
        DockerTransientNode node2 = TestableDockerContainerWatchdog.createMockedDockerTransientNode(containerId, nodeName2, cloud, true);
        allNodes.add(node2);

        subject.setAllNodes(allNodes);

        subject.runExecute();

        List<DockerTransientNode> nodes = subject.getAllRemovedNodes();
        Assert.assertEquals(1, nodes.size());

        DockerTransientNode removedNode = nodes.get(0);
        Assert.assertEquals(node1, removedNode);
    }

}
