package io.cameleer.server.app.runtime;

import io.cameleer.server.core.runtime.ContainerRequest;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.CreateVolumeCmd;
import com.github.dockerjava.api.command.InfoCmd;
import com.github.dockerjava.api.command.RemoveContainerCmd;
import com.github.dockerjava.api.command.RemoveVolumeCmd;
import com.github.dockerjava.api.command.StartContainerCmd;
import com.github.dockerjava.api.command.WaitContainerCmd;
import com.github.dockerjava.api.model.AccessMode;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.api.model.Info;
import com.github.dockerjava.core.command.WaitContainerResultCallback;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;

import java.util.List;
import java.util.Map;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Behaviour tests for the loader-init-container pattern (issue #152 close-out).
 * The orchestrator must:
 * <ol>
 *   <li>create a per-replica named volume,</li>
 *   <li>start the loader container with the volume mounted RW at /app/jars,</li>
 *   <li>wait for the loader to exit 0 before bringing up the main container,</li>
 *   <li>remove the loader (always) and remove the volume + abort if the loader fails,</li>
 *   <li>apply {@code withUsernsMode("host:1000:65536")} to BOTH containers.</li>
 * </ol>
 */
class DockerRuntimeOrchestratorLoaderTest {

    private static final String LOADER_IMAGE = "gitea.siegeln.net/cameleer/cameleer-runtime-loader:latest";
    private static final String BASE_IMAGE = "base-image:latest";
    private static final String CONTAINER_NAME = "tenant-app-0-abcd1234";
    private static final String EXPECTED_VOLUME = "cameleer-jars-" + CONTAINER_NAME;

    private static ContainerRequest sampleRequest() {
        return new ContainerRequest(
                CONTAINER_NAME,
                BASE_IMAGE,
                UUID.randomUUID(),
                "https://srv/api/v1/artifacts/uuid?exp=1&sig=x",
                12345L,
                LOADER_IMAGE,
                "tenant-net",
                List.of(),
                Map.of("CAMELEER_AGENT_APPLICATION", "myapp"),
                Map.of(),
                512L * 1024 * 1024,
                null,
                512,
                null,
                List.of(8080),
                9464,
                "on-failure",
                3,
                "spring-boot",
                "",
                null);
    }

    private static DockerClient mockDockerClientWithRuntimes(Map<String, ?> runtimes) {
        DockerClient dockerClient = mock(DockerClient.class);
        InfoCmd infoCmd = mock(InfoCmd.class);
        Info info = mock(Info.class);
        when(dockerClient.infoCmd()).thenReturn(infoCmd);
        when(infoCmd.exec()).thenReturn(info);
        when(info.getRuntimes()).thenReturn((Map) runtimes);
        return dockerClient;
    }

    /** Wires loader path + main path. Returns the loader create cmd, the main
     * create cmd, and the wait-callback so each test can poke them directly. */
    private static record Wired(CreateContainerCmd loaderCreate, CreateContainerCmd mainCreate,
                                 WaitContainerResultCallback waitCallback,
                                 RemoveVolumeCmd removeVolumeCmd) {}

    private static Wired wire(DockerClient dockerClient, int loaderExitCode) {
        CreateContainerCmd loaderCreate = mock(CreateContainerCmd.class, Answers.RETURNS_SELF);
        CreateContainerCmd mainCreate = mock(CreateContainerCmd.class, Answers.RETURNS_SELF);
        when(dockerClient.createContainerCmd(eq(LOADER_IMAGE))).thenReturn(loaderCreate);
        when(dockerClient.createContainerCmd(eq(BASE_IMAGE))).thenReturn(mainCreate);

        CreateContainerResponse loaderResp = mock(CreateContainerResponse.class);
        when(loaderResp.getId()).thenReturn("loader-id");
        when(loaderCreate.exec()).thenReturn(loaderResp);
        CreateContainerResponse mainResp = mock(CreateContainerResponse.class);
        when(mainResp.getId()).thenReturn("main-id");
        when(mainCreate.exec()).thenReturn(mainResp);

        when(dockerClient.startContainerCmd(anyString())).thenReturn(mock(StartContainerCmd.class));
        when(dockerClient.removeContainerCmd(anyString()))
                .thenReturn(mock(RemoveContainerCmd.class, Answers.RETURNS_SELF));

        when(dockerClient.createVolumeCmd()).thenReturn(mock(CreateVolumeCmd.class, Answers.RETURNS_SELF));
        RemoveVolumeCmd removeVolumeCmd = mock(RemoveVolumeCmd.class);
        when(dockerClient.removeVolumeCmd(anyString())).thenReturn(removeVolumeCmd);

        WaitContainerCmd waitCmd = mock(WaitContainerCmd.class);
        when(dockerClient.waitContainerCmd("loader-id")).thenReturn(waitCmd);
        WaitContainerResultCallback waitCallback = mock(WaitContainerResultCallback.class);
        when(waitCmd.exec(any())).thenReturn(waitCallback);
        when(waitCallback.awaitStatusCode(anyLong(), any())).thenReturn(loaderExitCode);

        return new Wired(loaderCreate, mainCreate, waitCallback, removeVolumeCmd);
    }

    @Test
    void startContainerCreatesVolumeRunsLoaderThenStartsMain() {
        DockerClient docker = mockDockerClientWithRuntimes(Map.of());
        Wired w = wire(docker, 0);

        var orchestrator = new DockerRuntimeOrchestrator(docker, "");
        String returnedId = orchestrator.startContainer(sampleRequest());

        assertThat(returnedId).isEqualTo("main-id");

        // Loader's HostConfig binds the named volume RW at /app/jars
        ArgumentCaptor<HostConfig> loaderHc = ArgumentCaptor.forClass(HostConfig.class);
        verify(w.loaderCreate()).withHostConfig(loaderHc.capture());
        assertThat(loaderHc.getValue().getBinds())
                .as("loader must mount the per-replica volume RW at /app/jars")
                .anyMatch(b -> b.getVolume().getPath().equals("/app/jars")
                        && b.getAccessMode() == AccessMode.rw
                        && b.getPath().equals(EXPECTED_VOLUME));

        // Main's HostConfig binds the same volume RO at /app/jars
        ArgumentCaptor<HostConfig> mainHc = ArgumentCaptor.forClass(HostConfig.class);
        verify(w.mainCreate()).withHostConfig(mainHc.capture());
        assertThat(mainHc.getValue().getBinds())
                .as("main must mount the per-replica volume RO at /app/jars")
                .anyMatch(b -> b.getVolume().getPath().equals("/app/jars")
                        && b.getAccessMode() == AccessMode.ro
                        && b.getPath().equals(EXPECTED_VOLUME));

        // Order: loader started, awaited, removed; then main started.
        InOrder order = inOrder(docker);
        order.verify(docker).createVolumeCmd();
        order.verify(docker).createContainerCmd(LOADER_IMAGE);
        order.verify(docker).startContainerCmd("loader-id");
        order.verify(docker).waitContainerCmd("loader-id");
        order.verify(docker).removeContainerCmd("loader-id");
        order.verify(docker).createContainerCmd(BASE_IMAGE);
        order.verify(docker).startContainerCmd("main-id");
    }

    @Test
    void startContainerAbortsAndCleansUpWhenLoaderFails() {
        DockerClient docker = mockDockerClientWithRuntimes(Map.of());
        Wired w = wire(docker, 2);

        var orchestrator = new DockerRuntimeOrchestrator(docker, "");

        assertThatThrownBy(() -> orchestrator.startContainer(sampleRequest()))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Loader exited 2");

        // Loader was removed, volume was removed, main container never created.
        verify(docker).removeContainerCmd("loader-id");
        verify(docker).removeVolumeCmd(EXPECTED_VOLUME);
        verify(w.removeVolumeCmd()).exec();
        verify(docker, never()).createContainerCmd(BASE_IMAGE);
    }

    @Test
    void startContainerAppliesUsernsModeToBothContainers() {
        DockerClient docker = mockDockerClientWithRuntimes(Map.of());
        Wired w = wire(docker, 0);

        var orchestrator = new DockerRuntimeOrchestrator(docker, "");
        orchestrator.startContainer(sampleRequest());

        ArgumentCaptor<HostConfig> loaderHc = ArgumentCaptor.forClass(HostConfig.class);
        verify(w.loaderCreate()).withHostConfig(loaderHc.capture());
        assertThat(loaderHc.getValue().getUsernsMode())
                .as("loader must run with the userns remap")
                .isEqualTo("host:1000:65536");

        ArgumentCaptor<HostConfig> mainHc = ArgumentCaptor.forClass(HostConfig.class);
        verify(w.mainCreate()).withHostConfig(mainHc.capture());
        assertThat(mainHc.getValue().getUsernsMode())
                .as("main must run with the userns remap")
                .isEqualTo("host:1000:65536");
    }
}
