"""Tests for SandboxBackend protocol and platform backends."""

from __future__ import annotations

import sys

import pytest

from avakill.core.models import (
    PolicyConfig,
    SandboxConfig,
    SandboxDenyPaths,
    SandboxNetworkRules,
    SandboxPathRules,
    SandboxResourceLimits,
)
from avakill.launcher.backends.base import SandboxBackend, get_sandbox_backend
from avakill.launcher.backends.darwin_backend import DarwinSandboxBackend
from avakill.launcher.backends.landlock_backend import LandlockBackend
from avakill.launcher.backends.macos_sandbox import MacOSSandboxBackend
from avakill.launcher.backends.noop import NoopSandboxBackend
from avakill.launcher.backends.windows_backend import WindowsSandboxBackend


class TestSandboxBackendProtocol:
    def test_noop_implements_protocol(self):
        backend = NoopSandboxBackend()
        assert isinstance(backend, SandboxBackend)

    def test_get_sandbox_backend_returns_backend(self):
        backend = get_sandbox_backend()
        assert isinstance(backend, SandboxBackend)

    def test_get_sandbox_backend_returns_noop_on_unknown_platform(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "freebsd")
        from avakill.launcher.backends.base import get_sandbox_backend as gsb

        backend = gsb()
        assert isinstance(backend, NoopSandboxBackend)


class TestNoopSandboxBackend:
    def test_available_returns_true(self):
        backend = NoopSandboxBackend()
        assert backend.available() is True

    def test_prepare_preexec_returns_none(self):
        backend = NoopSandboxBackend()
        config = SandboxConfig()
        assert backend.prepare_preexec(config) is None

    def test_prepare_process_args_returns_empty_dict(self):
        backend = NoopSandboxBackend()
        config = SandboxConfig()
        assert backend.prepare_process_args(config) == {}

    def test_post_create_is_noop(self):
        backend = NoopSandboxBackend()
        config = SandboxConfig()
        backend.post_create(12345, config)

    def test_describe_returns_platform_info(self):
        backend = NoopSandboxBackend()
        config = SandboxConfig()
        report = backend.describe(config)
        assert report["platform"] == "unsupported"
        assert report["sandbox_applied"] is False


class TestLandlockBackend:
    def test_available_true_on_linux_with_landlock(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "linux")
        monkeypatch.setattr(
            "avakill.launcher.backends.landlock_backend.LandlockEnforcer.available",
            staticmethod(lambda: True),
        )
        backend = LandlockBackend()
        assert backend.available() is True

    def test_available_false_on_non_linux(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "darwin")
        backend = LandlockBackend()
        assert backend.available() is False

    @pytest.mark.skipif(sys.platform == "win32", reason="Landlock requires Linux kernel")
    def test_prepare_preexec_returns_callable(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "linux")
        monkeypatch.setattr(
            "avakill.launcher.backends.landlock_backend.LandlockEnforcer.available",
            staticmethod(lambda: True),
        )
        backend = LandlockBackend()
        config = SandboxConfig(
            allow_paths=SandboxPathRules(
                read=["/usr", "/bin"],
                write=["/tmp"],
                execute=["/usr/bin/python3"],
            ),
        )
        fn = backend.prepare_preexec(config)
        assert callable(fn)

    def test_prepare_preexec_returns_none_when_unavailable(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "linux")
        monkeypatch.setattr(
            "avakill.launcher.backends.landlock_backend.LandlockEnforcer.available",
            staticmethod(lambda: False),
        )
        backend = LandlockBackend()
        config = SandboxConfig()
        assert backend.prepare_preexec(config) is None

    def test_prepare_process_args_returns_empty_dict(self):
        backend = LandlockBackend()
        config = SandboxConfig()
        assert backend.prepare_process_args(config) == {}

    def test_post_create_is_noop(self):
        backend = LandlockBackend()
        config = SandboxConfig()
        backend.post_create(12345, config)

    def test_describe_includes_abi_version(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "linux")
        monkeypatch.setattr(
            "avakill.launcher.backends.landlock_backend.LandlockEnforcer.available",
            staticmethod(lambda: True),
        )
        monkeypatch.setattr(
            "avakill.launcher.backends.landlock_backend.LandlockEnforcer.abi_version",
            staticmethod(lambda: 4),
        )
        backend = LandlockBackend()
        config = SandboxConfig(
            allow_paths=SandboxPathRules(read=["/usr"]),
        )
        report = backend.describe(config)
        assert report["platform"] == "linux"
        assert report["sandbox_applied"] is True
        assert report["abi_version"] == 4
        assert "/usr" in report["allowed_read_paths"]

    def test_describe_when_unavailable(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "linux")
        monkeypatch.setattr(
            "avakill.launcher.backends.landlock_backend.LandlockEnforcer.available",
            staticmethod(lambda: False),
        )
        backend = LandlockBackend()
        config = SandboxConfig()
        report = backend.describe(config)
        assert report["sandbox_applied"] is False


class TestDarwinSandboxBackend:
    def test_available_true_on_darwin(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "darwin")
        backend = DarwinSandboxBackend()
        assert backend.available() is True

    def test_available_false_on_non_darwin(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "linux")
        backend = DarwinSandboxBackend()
        assert backend.available() is False

    def test_prepare_preexec_returns_callable_on_darwin(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "darwin")
        backend = DarwinSandboxBackend()
        config = SandboxConfig(
            allow_paths=SandboxPathRules(read=["/usr"]),
        )
        fn = backend.prepare_preexec(config)
        assert callable(fn)

    def test_prepare_preexec_returns_none_on_non_darwin(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "linux")
        backend = DarwinSandboxBackend()
        config = SandboxConfig()
        assert backend.prepare_preexec(config) is None

    def test_prepare_process_args_returns_empty_dict(self):
        backend = DarwinSandboxBackend()
        config = SandboxConfig()
        assert backend.prepare_process_args(config) == {}

    def test_describe_includes_sbpl_preview(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "darwin")
        backend = DarwinSandboxBackend()
        config = SandboxConfig(
            allow_paths=SandboxPathRules(read=["/usr"], write=["/tmp"]),
        )
        report = backend.describe(config)
        assert report["platform"] == "darwin"
        assert report["sandbox_applied"] is True
        assert report["mechanism"] == "sandbox_init_with_parameters"
        assert "(deny default)" in report["sbpl_profile"]

    def test_describe_when_unavailable(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "linux")
        backend = DarwinSandboxBackend()
        config = SandboxConfig()
        report = backend.describe(config)
        assert report["sandbox_applied"] is False


class TestWindowsSandboxBackend:
    def test_available_true_on_windows(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "win32")
        backend = WindowsSandboxBackend()
        assert backend.available() is True

    def test_available_false_on_non_windows(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "linux")
        backend = WindowsSandboxBackend()
        assert backend.available() is False

    def test_prepare_preexec_returns_none(self):
        """Windows doesn't use preexec_fn."""
        backend = WindowsSandboxBackend()
        config = SandboxConfig()
        assert backend.prepare_preexec(config) is None

    def test_prepare_process_args_includes_creation_flags(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "win32")
        backend = WindowsSandboxBackend()
        config = SandboxConfig()
        args = backend.prepare_process_args(config)
        assert "creationflags" in args

    def test_prepare_process_args_empty_on_non_windows(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "linux")
        backend = WindowsSandboxBackend()
        config = SandboxConfig()
        assert backend.prepare_process_args(config) == {}

    def test_describe_includes_appcontainer_info(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "win32")
        backend = WindowsSandboxBackend()
        config = SandboxConfig(
            allow_paths=SandboxPathRules(read=["C:\\Users\\me\\project"]),
        )
        report = backend.describe(config)
        assert report["platform"] == "windows"
        assert report["sandbox_applied"] is True
        assert report["mechanism"] == "appcontainer"

    def test_describe_when_unavailable(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "linux")
        backend = WindowsSandboxBackend()
        config = SandboxConfig()
        report = backend.describe(config)
        assert report["sandbox_applied"] is False

    def test_describe_includes_resource_limits(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "win32")
        backend = WindowsSandboxBackend()
        config = SandboxConfig(
            resource_limits=SandboxResourceLimits(
                max_memory_mb=512,
                max_processes=10,
            ),
        )
        report = backend.describe(config)
        assert report["job_object"]["memory_limit_mb"] == 512
        assert report["job_object"]["process_limit"] == 10


class TestMacOSSandboxBackendModes:
    """Tests for MacOSSandboxBackend deny-default profile generation."""

    def test_always_generates_deny_default_profile(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "darwin")
        policy = PolicyConfig(
            version="1.0",
            default_action="allow",
            policies=[],
            sandbox=SandboxConfig(allow_paths=SandboxPathRules(write=["/tmp"])),
        )
        backend = MacOSSandboxBackend(policy)
        profile = backend.get_profile_content()
        assert "(deny default)" in profile

    def test_empty_sandbox_still_generates_profile(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "darwin")
        policy = PolicyConfig(
            version="1.0",
            default_action="allow",
            policies=[],
            sandbox=SandboxConfig(),
        )
        backend = MacOSSandboxBackend(policy)
        profile = backend.get_profile_content()
        assert "(deny default)" in profile

    def test_wrap_command_includes_dash_D_params(self, monkeypatch):
        monkeypatch.setattr("sys.platform", "darwin")
        monkeypatch.setattr("os.path.isfile", lambda p: True)
        policy = PolicyConfig(
            version="1.0",
            default_action="allow",
            policies=[],
            sandbox=SandboxConfig(allow_paths=SandboxPathRules(write=["/tmp"])),
        )
        backend = MacOSSandboxBackend(policy)
        wrapped = backend.wrap_command(["echo", "hello"], policy.sandbox)
        assert "/usr/bin/sandbox-exec" in wrapped
        assert "-D" in wrapped
        d_args = [wrapped[i + 1] for i, v in enumerate(wrapped) if v == "-D"]
        home_args = [a for a in d_args if a.startswith("HOME=")]
        assert len(home_args) == 1

    def test_describe_includes_write_and_deny_paths(self, monkeypatch):
        """describe() should report write paths and denied paths."""
        monkeypatch.setattr("sys.platform", "darwin")
        monkeypatch.setattr(
            "os.path.isfile",
            lambda p: p == "/usr/bin/sandbox-exec",
        )
        policy = PolicyConfig(
            version="1.0",
            default_action="allow",
            policies=[],
            sandbox=SandboxConfig(
                allow_paths=SandboxPathRules(write=["/tmp"]),
                deny_paths=SandboxDenyPaths(read=["~/.ssh"]),
            ),
        )
        backend = MacOSSandboxBackend(policy)
        report = backend.describe(policy.sandbox)
        assert report["sandbox_applied"] is True
        assert "/tmp" in report["allowed_write_paths"]
        assert "~/.ssh" in report["denied_read_paths"]

    def test_profile_includes_network(self, monkeypatch):
        """Profile should include network rules when configured."""
        monkeypatch.setattr("sys.platform", "darwin")
        policy = PolicyConfig(
            version="1.0",
            default_action="allow",
            policies=[],
            sandbox=SandboxConfig(
                allow_network=SandboxNetworkRules(connect=["api.openai.com:443"]),
            ),
        )
        backend = MacOSSandboxBackend(policy)
        profile = backend.get_profile_content()
        assert "(allow network-outbound" in profile


class TestSBPLProfileContent:
    """Tests for the rewritten SBPL profile generator."""

    def _config(self, **kwargs):
        return SandboxConfig(
            allow_paths=SandboxPathRules(write=["/tmp", "/Users/test/project"]),
            deny_paths=SandboxDenyPaths(read=["~/.ssh", "~/.aws"]),
            **kwargs,
        )

    def test_profile_starts_with_deny_default(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        profile = generate_sbpl_profile(self._config())
        assert "(deny default)" in profile

    def test_profile_has_process_operations(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        profile = generate_sbpl_profile(self._config())
        assert "(allow process-exec)" in profile
        assert "(allow process-fork)" in profile

    def test_profile_has_enumerated_mach_services(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        profile = generate_sbpl_profile(self._config())
        assert "com.apple.trustd" in profile
        assert "com.apple.cfprefsd.daemon" in profile
        assert "com.apple.system.opendirectoryd.libinfo" in profile
        lines = profile.split("\n")
        bare_mach = [line for line in lines if line.strip() == "(allow mach-lookup)"]
        assert bare_mach == []

    def test_profile_has_file_ioctl(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        profile = generate_sbpl_profile(self._config())
        assert "(allow file-ioctl)" in profile

    def test_profile_has_broad_home_read(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        profile = generate_sbpl_profile(self._config())
        assert '(subpath (param "HOME"))' in profile

    def test_profile_has_sensitive_denials(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        profile = generate_sbpl_profile(self._config())
        assert "(deny file-read*" in profile
        assert '(param "DENY_PATH_0")' in profile

    def test_profile_has_scoped_writes(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        profile = generate_sbpl_profile(self._config())
        assert '(param "WRITE_PATH_0")' in profile

    def test_profile_has_tmpdir_writes(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        profile = generate_sbpl_profile(self._config())
        assert "/private/var/folders" in profile
        assert "/private/tmp" in profile

    def test_profile_has_network_bind_for_oauth(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        config = self._config(
            allow_network=SandboxNetworkRules(
                connect=["api.anthropic.com:443"],
            ),
        )
        profile = generate_sbpl_profile(config)
        assert "network-bind" in profile
        assert "network-outbound" in profile

    def test_profile_has_mach_host_for_os_cpus(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        profile = generate_sbpl_profile(self._config())
        assert "(allow mach-host*)" in profile

    def test_profile_has_iokit(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        profile = generate_sbpl_profile(self._config())
        assert "(allow iokit-open)" in profile

    def test_profile_has_posix_shm(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        profile = generate_sbpl_profile(self._config())
        assert "ipc-posix-shm" in profile

    def test_profile_has_cryptexes_path(self):
        from avakill.launcher.backends.darwin_sbpl import generate_sbpl_profile

        profile = generate_sbpl_profile(self._config())
        assert "Cryptexes" in profile
