package com.cameleer.server.app.runtime;

import com.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.InfoCmd;
import com.github.dockerjava.api.command.StartContainerCmd;
import com.github.dockerjava.api.model.Capability;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.api.model.Info;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * Verifies the multi-tenant hardening contract from issue #152: every tenant
 * container is launched with cap_drop ALL, no-new-privileges, AppArmor profile,
 * read-only rootfs, a pids limit, and a writeable /tmp tmpfs. Also verifies the
 * runsc auto-detect via `docker info` and the explicit override.
 */
class DockerRuntimeOrchestratorHardeningTest {

    private static ContainerRequest sampleRequest() {
        return new ContainerRequest(
                "tenant-app-0-abcd1234",
                "registry.example/runtime:latest",
                "/data/jars/app.jar",
                null, null,
                "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;
    }

    @Test
    void resolveRuntime_picksRunscWhenDaemonHasIt() {
        DockerClient dockerClient = mockDockerClientWithRuntimes(Map.of(
                "runc", new Object(),
                "runsc", new Object()));

        DockerRuntimeOrchestrator orchestrator = new DockerRuntimeOrchestrator(dockerClient, "");

        assertThat(orchestrator.getDockerRuntime()).isEqualTo("runsc");
    }

    @Test
    void resolveRuntime_returnsEmptyWhenSandboxedRuntimeMissing() {
        DockerClient dockerClient = mockDockerClientWithRuntimes(Map.of("runc", new Object()));

        DockerRuntimeOrchestrator orchestrator = new DockerRuntimeOrchestrator(dockerClient, "");

        assertThat(orchestrator.getDockerRuntime()).isEmpty();
    }

    @Test
    void resolveRuntime_overrideWinsOverAutoDetect() {
        DockerClient dockerClient = mockDockerClientWithRuntimes(Map.of(
                "runc", new Object(),
                "runsc", new Object()));

        DockerRuntimeOrchestrator orchestrator = new DockerRuntimeOrchestrator(dockerClient, "kata");

        assertThat(orchestrator.getDockerRuntime()).isEqualTo("kata");
    }

    @Test
    void resolveRuntime_blankOverrideTreatedAsAuto() {
        DockerClient dockerClient = mockDockerClientWithRuntimes(Map.of("runsc", new Object()));

        DockerRuntimeOrchestrator orchestrator = new DockerRuntimeOrchestrator(dockerClient, "   ");

        assertThat(orchestrator.getDockerRuntime()).isEqualTo("runsc");
    }

    @Test
    void resolveRuntime_swallowsDockerInfoFailure() {
        DockerClient dockerClient = mock(DockerClient.class);
        InfoCmd infoCmd = mock(InfoCmd.class);
        when(dockerClient.infoCmd()).thenReturn(infoCmd);
        when(infoCmd.exec()).thenThrow(new RuntimeException("docker daemon unreachable"));

        DockerRuntimeOrchestrator orchestrator = new DockerRuntimeOrchestrator(dockerClient, "");

        assertThat(orchestrator.getDockerRuntime()).isEmpty();
    }

    @Test
    void startContainer_appliesHardeningContractToHostConfig() {
        DockerClient dockerClient = mockDockerClientWithRuntimes(new HashMap<>());

        CreateContainerCmd createCmd = mock(CreateContainerCmd.class, Answers.RETURNS_SELF);
        when(dockerClient.createContainerCmd(anyString())).thenReturn(createCmd);
        CreateContainerResponse createResponse = mock(CreateContainerResponse.class);
        when(createResponse.getId()).thenReturn("container-id-1");
        when(createCmd.exec()).thenReturn(createResponse);
        StartContainerCmd startCmd = mock(StartContainerCmd.class);
        when(dockerClient.startContainerCmd(anyString())).thenReturn(startCmd);

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

        ArgumentCaptor<HostConfig> hostCaptor = ArgumentCaptor.forClass(HostConfig.class);
        org.mockito.Mockito.verify(createCmd).withHostConfig(hostCaptor.capture());
        HostConfig hc = hostCaptor.getValue();

        // cap_drop ALL — every capability the SDK knows about
        assertThat(hc.getCapDrop())
                .as("cap_drop should drop every capability")
                .containsExactlyInAnyOrder(Capability.values());

        // no-new-privileges + apparmor stock profile
        assertThat(hc.getSecurityOpts())
                .as("security_opt must include no-new-privileges and apparmor=docker-default")
                .contains("no-new-privileges:true", "apparmor=docker-default");

        // readonly rootfs
        assertThat(hc.getReadonlyRootfs())
                .as("read_only rootfs must be enabled")
                .isTrue();

        // pids-limit applied
        assertThat(hc.getPidsLimit())
                .as("pids_limit must be set to bound fork-bomb damage")
                .isNotNull()
                .isPositive();

        // /tmp tmpfs writable, nosuid, no `noexec` (would break JNI dlopen)
        assertThat(hc.getTmpFs())
                .as("/tmp must be a writeable tmpfs")
                .containsKey("/tmp");
        String tmpOpts = hc.getTmpFs().get("/tmp");
        assertThat(tmpOpts).contains("rw").contains("nosuid").doesNotContain("noexec");
    }

    @Test
    void startContainer_doesNotForceRuntimeWhenAutoDetectFindsNothing() {
        DockerClient dockerClient = mockDockerClientWithRuntimes(Map.of("runc", new Object()));
        CreateContainerCmd createCmd = mock(CreateContainerCmd.class, Answers.RETURNS_SELF);
        when(dockerClient.createContainerCmd(anyString())).thenReturn(createCmd);
        CreateContainerResponse createResponse = mock(CreateContainerResponse.class);
        when(createResponse.getId()).thenReturn("c");
        when(createCmd.exec()).thenReturn(createResponse);
        when(dockerClient.startContainerCmd(anyString())).thenReturn(mock(StartContainerCmd.class));

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

        ArgumentCaptor<HostConfig> hostCaptor = ArgumentCaptor.forClass(HostConfig.class);
        org.mockito.Mockito.verify(createCmd).withHostConfig(hostCaptor.capture());

        // When daemon has no sandboxed runtime, we leave runtime null/empty so Docker picks its default.
        String runtime = hostCaptor.getValue().getRuntime();
        assertThat(runtime == null || runtime.isBlank())
                .as("no runtime should be forced when sandboxed runtime unavailable")
                .isTrue();
    }

    @Test
    void startContainer_appliesRunscWhenAvailable() {
        DockerClient dockerClient = mockDockerClientWithRuntimes(Map.of("runsc", new Object()));
        CreateContainerCmd createCmd = mock(CreateContainerCmd.class, Answers.RETURNS_SELF);
        when(dockerClient.createContainerCmd(anyString())).thenReturn(createCmd);
        CreateContainerResponse createResponse = mock(CreateContainerResponse.class);
        when(createResponse.getId()).thenReturn("c");
        when(createCmd.exec()).thenReturn(createResponse);
        when(dockerClient.startContainerCmd(anyString())).thenReturn(mock(StartContainerCmd.class));

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

        ArgumentCaptor<HostConfig> hostCaptor = ArgumentCaptor.forClass(HostConfig.class);
        org.mockito.Mockito.verify(createCmd).withHostConfig(hostCaptor.capture());

        assertThat(hostCaptor.getValue().getRuntime()).isEqualTo("runsc");
    }
}
