"""Unit tests for the manifest compiler."""

import pytest

from src.services.build.manifest_compiler import (
    compile_compose,
    compile_dockerfile,
    compile_entrypoint,
    get_manifest_home_dir,
)


@pytest.mark.unit
def test_compile_dockerfile_creates_config_dirs_for_user() -> None:
    """The Dockerfile must pre-create ~/.config and chown it to the container user."""
    manifest = {
        "base_image": "ubuntu:24.04",
        "interface_type": "terminal",
        "user": {"name": "dev", "uid": 1000, "gid": 1000},
    }

    dockerfile = compile_dockerfile(manifest)

    assert "groupadd -g 1000 dev" in dockerfile
    assert "useradd -u 1000 -g 1000 -m -s /bin/bash dev" in dockerfile
    assert "mkdir -p /home/dev && chown -R dev:dev /home/dev" in dockerfile
    assert (
        "mkdir -p /home/dev/.config && chown -R dev:dev /home/dev/.config" in dockerfile
    )
    assert (
        "mkdir -p /home/dev/.local/share && chown -R dev:dev /home/dev/.local/share"
        in dockerfile
    )
    assert (
        "mkdir -p /home/dev/.cache && chown -R dev:dev /home/dev/.cache" in dockerfile
    )


@pytest.mark.unit
def test_compile_dockerfile_no_user_does_not_create_home() -> None:
    """Without a user config, no home/user setup should be emitted."""
    manifest = {
        "base_image": "ubuntu:24.04",
        "interface_type": "terminal",
    }

    dockerfile = compile_dockerfile(manifest)

    assert "useradd" not in dockerfile
    assert "/home/" not in dockerfile


@pytest.mark.unit
class TestGetManifestHomeDir:
    """Tests for get_manifest_home_dir precedence."""

    def test_home_directory_in_manifest_wins(self) -> None:
        manifest = {
            "home_directory": "/home/custom",
            "user": {"name": "dev"},
        }
        assert get_manifest_home_dir(manifest) == "/home/custom"

    def test_user_name_derives_home(self) -> None:
        manifest = {"user": {"name": "dev", "uid": 1000, "gid": 1000}}
        assert get_manifest_home_dir(manifest) == "/home/dev"

    def test_root_fallback(self) -> None:
        manifest = {"base_image": "ubuntu:24.04"}
        assert get_manifest_home_dir(manifest) == "/root"

    def test_empty_home_directory_falls_back(self) -> None:
        manifest = {"home_directory": "", "user": {"name": "dev"}}
        assert get_manifest_home_dir(manifest) == "/home/dev"


@pytest.mark.unit
class TestCompileDockerfileHomeDirectory:
    """Tests that compile_dockerfile honors manifest.home_directory."""

    def test_env_home_and_workdir_use_home_directory(self) -> None:
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
        }
        dockerfile = compile_dockerfile(manifest)

        assert "ENV HOME=/home/custom" in dockerfile
        assert "WORKDIR /home/custom" in dockerfile

    def test_workspace_symlink_created(self) -> None:
        """When workspace name is known at build time, create /workspace symlink."""
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "workspace_name": "my-app",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
        }
        dockerfile = compile_dockerfile(manifest)

        assert "mkdir -p /home/custom" in dockerfile
        assert "ln -sfn /home/custom/my-app /workspace" in dockerfile

    def test_runtime_workspace_not_baked_into_image(self) -> None:
        """When workspace name is a runtime placeholder, do not create literal
        {{WORKSPACE_NAME}} directories or symlinks in the image."""
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
            "mounts": [{"source_type": "repo", "target": "~/{{WORKSPACE_NAME}}"}],
        }
        dockerfile = compile_dockerfile(manifest)

        assert "{{WORKSPACE_NAME}}" not in dockerfile
        assert "ln -sfn" not in dockerfile

    def test_runtime_working_dir_overrides_home_workdir(self) -> None:
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
            "runtime": {"working_dir": "/app/code"},
        }
        dockerfile = compile_dockerfile(manifest)

        assert "WORKDIR /app/code" in dockerfile
        assert "WORKDIR /home/custom" not in dockerfile

    def test_working_dir_expands_tilde(self) -> None:
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
            "runtime": {"working_dir": "~/code"},
        }
        dockerfile = compile_dockerfile(manifest)

        assert "WORKDIR /home/custom/code" in dockerfile


@pytest.mark.unit
class TestCompileComposeHomeDirectory:
    """Tests that compile_compose uses home_directory for volumes and working_dir."""

    def test_default_repo_mount_synthesized(self) -> None:
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
        }
        variables = {
            "IMAGE_TAG": "test:latest",
            "INSTANCE_NAME": "test-instance",
            "REPO_PATH": "/host/repos/my-app",
            "WORKSPACE_NAME": "my-app",
            "TOOL_PORT": 0,
            "EXTRA_ENV": {},
            "EXTRA_VOLUMES": [],
        }
        compose = compile_compose(manifest, variables)

        assert "/host/repos/my-app:/home/custom/my-app" in compose

    def test_explicit_repo_mount_preserved(self) -> None:
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
            "mounts": [
                {"source_type": "repo", "target": "/opt/code", "readonly": True}
            ],
        }
        variables = {
            "IMAGE_TAG": "test:latest",
            "INSTANCE_NAME": "test-instance",
            "REPO_PATH": "/host/repos/my-app",
            "WORKSPACE_NAME": "my-app",
            "TOOL_PORT": 0,
            "EXTRA_ENV": {},
            "EXTRA_VOLUMES": [],
        }
        compose = compile_compose(manifest, variables)

        assert "/host/repos/my-app:/opt/code:ro" in compose
        assert "/home/custom/my-app" not in compose

    def test_workspace_name_substituted_in_mount_target(self) -> None:
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
            "mounts": [{"source_type": "repo", "target": "~/{{WORKSPACE_NAME}}"}],
        }
        variables = {
            "IMAGE_TAG": "test:latest",
            "INSTANCE_NAME": "test-instance",
            "REPO_PATH": "/host/repos/my-app",
            "WORKSPACE_NAME": "my-app",
            "TOOL_PORT": 0,
            "EXTRA_ENV": {},
            "EXTRA_VOLUMES": [],
        }
        compose = compile_compose(manifest, variables)

        assert "/host/repos/my-app:/home/custom/my-app" in compose
        assert "WORKSPACE_NAME: my-app" in compose

    def test_working_dir_expands_home(self) -> None:
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
            "runtime": {"working_dir": "$HOME/code"},
        }
        variables = {
            "IMAGE_TAG": "test:latest",
            "INSTANCE_NAME": "test-instance",
            "REPO_PATH": "/host/repos/my-app",
            "WORKSPACE_NAME": "my-app",
            "TOOL_PORT": 0,
            "EXTRA_ENV": {},
            "EXTRA_VOLUMES": [],
        }
        compose = compile_compose(manifest, variables)

        assert "working_dir: /home/custom/code" in compose


@pytest.mark.unit
def test_compile_dockerfile_uses_user_npm_prefix() -> None:
    """npm global packages must be installed into a user-writable prefix."""
    manifest = {
        "base_image": "ubuntu:24.04",
        "interface_type": "terminal",
        "home_directory": "/home/custom",
        "user": {"name": "dev", "uid": 1000, "gid": 1000},
        "packages": {"npm_global": ["@scope/pkg"]},
    }
    dockerfile = compile_dockerfile(manifest)

    assert "npm install -g --prefix /home/custom/.npm-global" in dockerfile
    assert "/home/custom/.npm-global/bin:$PATH" in dockerfile
    assert "ENV PATH=/home/custom/.npm-global/bin:$PATH" in dockerfile


@pytest.mark.unit
def test_compile_dockerfile_starts_as_root_and_drops_privileges() -> None:
    """The Dockerfile must not set USER so the entrypoint starts as root.

    The entrypoint itself drops privileges to the container user before
    exec-ing the real command.
    """
    manifest = {
        "base_image": "ubuntu:24.04",
        "interface_type": "terminal",
        "user": {"name": "dev", "uid": 1000, "gid": 1000},
    }
    dockerfile = compile_dockerfile(manifest)
    entrypoint = compile_entrypoint(manifest)

    assert "USER dev" not in dockerfile
    assert 'exec su -s /bin/bash -c "exec \\"$@\\"" dev -- "$@"' in entrypoint


@pytest.mark.unit
def test_compile_compose_runs_as_root() -> None:
    """The compose service must start as root so the entrypoint can fix /workspace."""
    manifest = {
        "base_image": "ubuntu:24.04",
        "interface_type": "terminal",
        "user": {"name": "dev", "uid": 1000, "gid": 1000},
    }
    variables = {
        "IMAGE_TAG": "test:latest",
        "INSTANCE_NAME": "test-instance",
        "REPO_PATH": "/host/repos/my-app",
        "WORKSPACE_NAME": "my-app",
        "TOOL_PORT": 0,
        "EXTRA_ENV": {},
        "EXTRA_VOLUMES": [],
    }
    compose = compile_compose(manifest, variables)

    assert "user: 0:0" in compose


@pytest.mark.unit
class TestCompileEntrypoint:
    """Tests for the generated permission-fixing entrypoint."""

    def test_entrypoint_creates_home_and_workspace(self) -> None:
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
        }
        entrypoint = compile_entrypoint(manifest)

        assert 'mkdir -p "$HOME_DIR"' in entrypoint
        assert 'mkdir -p "$WORKSPACE_TARGET"' in entrypoint
        assert 'ln -sfn "$WORKSPACE_TARGET" /workspace' in entrypoint
        assert 'WORKSPACE_NAME="${WORKSPACE_NAME:-workspace}"' in entrypoint

    def test_entrypoint_removes_stale_placeholder_directory(self) -> None:
        """Older images baked in a literal {{WORKSPACE_NAME}} directory."""
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
        }
        entrypoint = compile_entrypoint(manifest)

        assert 'if [ -d "${HOME_DIR}/{{WORKSPACE_NAME}}" ]; then' in entrypoint
        assert 'rm -rf "${HOME_DIR}/{{WORKSPACE_NAME}}"' in entrypoint

    def test_entrypoint_uses_root_then_sudo_for_workspace_symlink(self) -> None:
        """/workspace is under /, so root takes precedence; non-root falls back to sudo."""
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
        }
        entrypoint = compile_entrypoint(manifest)

        root_idx = entrypoint.find('if [ "$(id -u)" = "0" ]; then')
        sudo_idx = entrypoint.find('elif [ -n "$SUDO" ]; then')
        assert root_idx != -1
        assert sudo_idx != -1
        assert root_idx < sudo_idx
        assert 'sudo ln -sfn "$WORKSPACE_TARGET" /workspace' in entrypoint

    def test_entrypoint_fixes_mount_owners(self) -> None:
        manifest = {
            "base_image": "ubuntu:24.04",
            "interface_type": "terminal",
            "home_directory": "/home/custom",
            "user": {"name": "dev", "uid": 1000, "gid": 1000},
            "mounts": [
                {"target": "~/.config", "readonly": False},
                {"target": "/opt/readonly", "readonly": True},
            ],
        }
        entrypoint = compile_entrypoint(manifest)

        assert 'fix_owner "/home/custom/.config"' in entrypoint
        assert 'fix_owner "/opt/readonly"' not in entrypoint
