"""Tests for the security monitoring module."""

from __future__ import annotations

import json
import tempfile
from datetime import UTC, datetime
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from gaol.security.apparmor import (
    DEFAULT_CAPABILITIES_ALLOW,
    DEFAULT_CAPABILITIES_DENY,
    DEFAULT_FILE_DENY,
    DEFAULT_FILE_READ,
    AppArmorProfileGenerator,
)
from gaol.security.client import (
    FalcoClient,
    FalcoEventBuffer,
)
from gaol.security.exceptions import (
    FalcoConnectionError,
    FalcoNotInstalledError,
    FalcoNotRunningError,
    RuleExistsError,
    RuleNotFoundError,
    RuleValidationError,
    SecurityError,
    UnsupportedDistroError,
)
from gaol.security.installer import (
    FalcoInstaller,
    PackageManager,
)
from gaol.security.models import (
    AppArmorMode,
    AppArmorProfile,
    ContainerInfo,
    EventSummary,
    FalcoPriority,
    FalcoStatus,
    RuleList,
    SeccompAction,
    SeccompProfile,
    SeccompSyscall,
    SecurityEvent,
    SecurityRule,
)
from gaol.security.rules import (
    RuleManager,
)
from gaol.security.seccomp import (
    BASELINE_SYSCALLS,
    SeccompProfileGenerator,
)

# =============================================================================
# Model Tests
# =============================================================================


class TestFalcoPriority:
    """Tests for FalcoPriority enum."""

    def test_from_string_valid(self) -> None:
        """Test parsing valid priority strings."""
        assert FalcoPriority.from_string("Emergency") == FalcoPriority.EMERGENCY
        assert FalcoPriority.from_string("warning") == FalcoPriority.WARNING
        assert FalcoPriority.from_string("DEBUG") == FalcoPriority.DEBUG

    def test_from_string_invalid(self) -> None:
        """Test parsing invalid priority strings raises ValueError."""
        with pytest.raises(ValueError, match="Unknown priority"):
            FalcoPriority.from_string("invalid")

    def test_priority_comparison(self) -> None:
        """Test priority comparison (Emergency is highest)."""
        assert FalcoPriority.EMERGENCY < FalcoPriority.ALERT
        assert FalcoPriority.ALERT < FalcoPriority.CRITICAL
        assert FalcoPriority.WARNING < FalcoPriority.DEBUG
        assert FalcoPriority.EMERGENCY <= FalcoPriority.EMERGENCY
        assert FalcoPriority.WARNING <= FalcoPriority.DEBUG

    def test_priority_comparison_with_non_priority(self) -> None:
        """Test comparison with non-FalcoPriority returns NotImplemented."""
        assert FalcoPriority.WARNING.__lt__("string") == NotImplemented
        assert FalcoPriority.WARNING.__le__(123) == NotImplemented


class TestContainerInfo:
    """Tests for ContainerInfo model."""

    def test_container_info_creation(self) -> None:
        """Test creating container info."""
        info = ContainerInfo(
            id="abc123",
            name="my-container",
            image="nginx",
            image_tag="latest",
        )
        assert info.id == "abc123"
        assert info.name == "my-container"
        assert info.image == "nginx"
        assert info.image_tag == "latest"

    def test_container_info_defaults(self) -> None:
        """Test container info defaults to None."""
        info = ContainerInfo()
        assert info.id is None
        assert info.name is None
        assert info.image is None


class TestSecurityEvent:
    """Tests for SecurityEvent model."""

    def test_security_event_creation(self) -> None:
        """Test creating a security event."""
        event = SecurityEvent(
            uuid="test-uuid",
            timestamp=datetime.now(UTC),
            rule="test_rule",
            priority=FalcoPriority.WARNING,
            output="Test output message",
        )
        assert event.uuid == "test-uuid"
        assert event.rule == "test_rule"
        assert event.priority == FalcoPriority.WARNING

    def test_matches_container(self) -> None:
        """Test container name pattern matching."""
        event = SecurityEvent(
            uuid="test",
            timestamp=datetime.now(UTC),
            rule="test",
            priority=FalcoPriority.WARNING,
            output="test",
            container=ContainerInfo(name="my-app-container"),
        )
        assert event.matches_container("my-app.*")
        assert event.matches_container("my-app-container")
        assert not event.matches_container("other-.*")

    def test_matches_container_no_container(self) -> None:
        """Test matching when no container info."""
        event = SecurityEvent(
            uuid="test",
            timestamp=datetime.now(UTC),
            rule="test",
            priority=FalcoPriority.WARNING,
            output="test",
        )
        assert not event.matches_container(".*")

    def test_is_at_least_priority(self) -> None:
        """Test priority level checking."""
        event = SecurityEvent(
            uuid="test",
            timestamp=datetime.now(UTC),
            rule="test",
            priority=FalcoPriority.WARNING,
            output="test",
        )
        # WARNING is at least WARNING
        assert event.is_at_least_priority(FalcoPriority.WARNING)
        # WARNING is at least DEBUG (lower priority)
        assert event.is_at_least_priority(FalcoPriority.DEBUG)
        # WARNING is NOT at least ERROR (higher priority)
        assert not event.is_at_least_priority(FalcoPriority.ERROR)


class TestSecurityRule:
    """Tests for SecurityRule model."""

    def test_security_rule_creation(self) -> None:
        """Test creating a security rule."""
        rule = SecurityRule(
            name="test_rule",
            description="A test rule",
            condition="container and proc.name=bash",
            output="Shell spawned",
            priority=FalcoPriority.WARNING,
            tags=["container", "shell"],
        )
        assert rule.name == "test_rule"
        assert rule.enabled is True
        assert rule.gaol_managed is True

    def test_to_falco_yaml(self) -> None:
        """Test converting rule to Falco YAML format."""
        rule = SecurityRule(
            name="test_rule",
            description="Test description",
            condition="container",
            output="Output message",
            priority=FalcoPriority.CRITICAL,
            tags=["test"],
        )
        yaml_dict = rule.to_falco_yaml()

        assert yaml_dict["rule"] == "test_rule"
        assert yaml_dict["desc"] == "Test description"
        assert yaml_dict["condition"] == "container"
        assert yaml_dict["output"] == "Output message"
        assert yaml_dict["priority"] == "Critical"
        assert yaml_dict["tags"] == ["test"]
        assert yaml_dict["enabled"] is True


class TestRuleList:
    """Tests for RuleList model."""

    def test_rule_list_to_falco_yaml(self) -> None:
        """Test converting rule list to Falco YAML format."""
        rule_list = RuleList(
            macros={"container": "container.id != host"},
            lists={"shells": ["bash", "sh", "zsh"]},
            rules=[
                SecurityRule(
                    name="test_rule",
                    condition="container",
                    output="test",
                    priority=FalcoPriority.WARNING,
                )
            ],
        )
        yaml_items = rule_list.to_falco_yaml()

        # Should have: engine version, macro, list, rule
        assert len(yaml_items) == 4
        assert yaml_items[0] == {"required_engine_version": "0.26.0"}
        assert yaml_items[1] == {"macro": "container", "condition": "container.id != host"}
        assert yaml_items[2] == {"list": "shells", "items": ["bash", "sh", "zsh"]}
        assert yaml_items[3]["rule"] == "test_rule"


class TestSeccompProfile:
    """Tests for SeccompProfile model."""

    def test_seccomp_profile_creation(self) -> None:
        """Test creating a seccomp profile."""
        profile = SeccompProfile(
            name="test-profile",
            defaultAction=SeccompAction.SCMP_ACT_ERRNO,
            syscalls=[
                SeccompSyscall(
                    names=["read", "write"],
                    action=SeccompAction.SCMP_ACT_ALLOW,
                )
            ],
        )
        assert profile.name == "test-profile"
        assert profile.defaultAction == SeccompAction.SCMP_ACT_ERRNO
        assert len(profile.syscalls) == 1

    def test_to_oci_json(self) -> None:
        """Test converting to OCI JSON format."""
        profile = SeccompProfile(
            defaultAction=SeccompAction.SCMP_ACT_ERRNO,
            syscalls=[
                SeccompSyscall(
                    names=["read", "write"],
                    action=SeccompAction.SCMP_ACT_ALLOW,
                )
            ],
        )
        oci = profile.to_oci_json()

        assert oci["defaultAction"] == "SCMP_ACT_ERRNO"
        assert len(oci["syscalls"]) == 1
        assert oci["syscalls"][0]["names"] == ["read", "write"]
        assert oci["syscalls"][0]["action"] == "SCMP_ACT_ALLOW"

    def test_add_syscall_new_action(self) -> None:
        """Test adding syscalls with new action."""
        profile = SeccompProfile()
        profile.add_syscall(["read", "write"], SeccompAction.SCMP_ACT_ALLOW)

        assert len(profile.syscalls) == 1
        assert "read" in profile.syscalls[0].names
        assert "write" in profile.syscalls[0].names

    def test_add_syscall_existing_action(self) -> None:
        """Test adding syscalls to existing action group."""
        profile = SeccompProfile(
            syscalls=[
                SeccompSyscall(names=["read"], action=SeccompAction.SCMP_ACT_ALLOW)
            ]
        )
        profile.add_syscall(["write"], SeccompAction.SCMP_ACT_ALLOW)

        assert len(profile.syscalls) == 1
        assert "read" in profile.syscalls[0].names
        assert "write" in profile.syscalls[0].names


class TestAppArmorProfile:
    """Tests for AppArmorProfile model."""

    def test_apparmor_profile_creation(self) -> None:
        """Test creating an AppArmor profile."""
        profile = AppArmorProfile(
            name="test-profile",
            mode=AppArmorMode.ENFORCE,
            capabilities_allow=["chown", "setuid"],
            file_read=["/bin/**"],
        )
        assert profile.name == "test-profile"
        assert profile.mode == AppArmorMode.ENFORCE
        assert "chown" in profile.capabilities_allow

    def test_to_apparmor_text(self) -> None:
        """Test generating AppArmor profile text."""
        profile = AppArmorProfile(
            name="test_profile",
            capabilities_allow=["chown"],
            capabilities_deny=["sys_admin"],
            file_read=["/bin/**"],
            file_write=["/tmp/**"],
            network_allow=["inet stream"],
        )
        text = profile.to_apparmor_text()

        assert "profile test_profile" in text
        assert "capability chown," in text
        assert "deny capability sys_admin," in text
        assert "/bin/** r," in text
        assert "/tmp/** w," in text
        assert "network inet stream," in text

    def test_capabilities_lowercase(self) -> None:
        """Test capabilities are lowercased."""
        profile = AppArmorProfile(
            name="test",
            capabilities_allow=["CHOWN", "SetUID"],
        )
        assert profile.capabilities_allow == ["chown", "setuid"]


class TestFalcoStatus:
    """Tests for FalcoStatus model."""

    def test_falco_status_defaults(self) -> None:
        """Test FalcoStatus default values."""
        status = FalcoStatus()
        assert status.installed is False
        assert status.running is False
        assert status.version is None
        assert status.grpc_enabled is False


class TestEventSummary:
    """Tests for EventSummary model."""

    def test_event_summary_defaults(self) -> None:
        """Test EventSummary default values."""
        summary = EventSummary()
        assert summary.total_events == 0
        assert summary.events_by_priority == {}
        assert summary.events_by_rule == {}
        assert summary.events_by_container == {}


# =============================================================================
# Exception Tests
# =============================================================================


class TestSecurityExceptions:
    """Tests for security exceptions."""

    def test_security_error_base(self) -> None:
        """Test SecurityError is base exception."""
        with pytest.raises(SecurityError):
            raise FalcoConnectionError("test")

    def test_unsupported_distro_error(self) -> None:
        """Test UnsupportedDistroError message."""
        error = UnsupportedDistroError("arch")
        assert "arch" in str(error)
        assert error.distro == "arch"

    def test_rule_validation_error(self) -> None:
        """Test RuleValidationError with errors list."""
        error = RuleValidationError("test_rule", ["error1", "error2"])
        assert "test_rule" in str(error)
        assert error.name == "test_rule"
        assert len(error.errors) == 2

    def test_rule_not_found_error(self) -> None:
        """Test RuleNotFoundError."""
        error = RuleNotFoundError("missing_rule")
        assert "missing_rule" in str(error)
        assert error.name == "missing_rule"

    def test_rule_exists_error(self) -> None:
        """Test RuleExistsError."""
        error = RuleExistsError("existing_rule")
        assert "existing_rule" in str(error)
        assert error.name == "existing_rule"


# =============================================================================
# FalcoClient Tests
# =============================================================================


class TestFalcoClient:
    """Tests for FalcoClient."""

    def test_client_creation(self) -> None:
        """Test creating a client with default settings."""
        client = FalcoClient()
        assert client.is_connected() is False
        assert "unix://" in client.endpoint

    def test_client_with_grpc_address(self) -> None:
        """Test creating client with gRPC address."""
        client = FalcoClient(grpc_address="localhost:5060")
        assert client.endpoint == "localhost:5060"

    def test_client_with_socket_path(self) -> None:
        """Test creating client with custom socket path."""
        client = FalcoClient(socket_path="/custom/socket.sock")
        assert "/custom/socket.sock" in client.endpoint

    @patch.object(FalcoClient, "_is_falco_installed")
    def test_connect_not_installed(self, mock_installed: MagicMock) -> None:
        """Test connect raises when Falco not installed."""
        mock_installed.return_value = False
        client = FalcoClient()

        with pytest.raises(FalcoNotInstalledError):
            client.connect()

    @patch.object(FalcoClient, "_is_falco_installed")
    @patch.object(FalcoClient, "_is_falco_running")
    def test_connect_not_running(
        self, mock_running: MagicMock, mock_installed: MagicMock
    ) -> None:
        """Test connect raises when Falco not running."""
        mock_installed.return_value = True
        mock_running.return_value = False
        client = FalcoClient()

        with pytest.raises(FalcoNotRunningError):
            client.connect()

    def test_disconnect(self) -> None:
        """Test disconnecting client."""
        client = FalcoClient()
        client._connected = True
        client.disconnect()
        assert client.is_connected() is False

    def test_get_status_not_installed(self) -> None:
        """Test get_status when Falco not installed."""
        with patch.object(FalcoClient, "_is_falco_installed", return_value=False):
            client = FalcoClient()
            status = client.get_status()
            assert status.installed is False
            assert status.running is False

    def test_parse_event_line_json(self) -> None:
        """Test parsing JSON event line."""
        client = FalcoClient()
        json_line = json.dumps({
            "uuid": "test-uuid",
            "time": "2024-01-01T00:00:00Z",
            "rule": "test_rule",
            "priority": "Warning",
            "output": "Test output",
            "source": "syscall",
        })

        event = client._parse_event_line(json_line)
        assert event is not None
        assert event.uuid == "test-uuid"
        assert event.rule == "test_rule"
        assert event.priority == FalcoPriority.WARNING

    def test_parse_event_line_invalid(self) -> None:
        """Test parsing invalid event line is handled."""
        client = FalcoClient()
        # The parser is lenient and may return None or attempt to parse
        # We just verify it doesn't crash on invalid input
        result = client._parse_event_line("not a valid event")
        # Result can be None or a partially parsed event
        assert result is None or isinstance(result, SecurityEvent)


class TestFalcoEventBuffer:
    """Tests for FalcoEventBuffer."""

    def test_buffer_creation(self) -> None:
        """Test creating an event buffer."""
        buffer = FalcoEventBuffer(max_size=100)
        assert len(buffer) == 0

    def test_add_event(self) -> None:
        """Test adding events to buffer."""
        buffer = FalcoEventBuffer()
        event = SecurityEvent(
            uuid="test",
            timestamp=datetime.now(UTC),
            rule="test",
            priority=FalcoPriority.WARNING,
            output="test",
        )
        buffer.add(event)
        assert len(buffer) == 1

    def test_buffer_max_size(self) -> None:
        """Test buffer respects max size."""
        buffer = FalcoEventBuffer(max_size=2)

        for i in range(5):
            event = SecurityEvent(
                uuid=f"test-{i}",
                timestamp=datetime.now(UTC),
                rule="test",
                priority=FalcoPriority.WARNING,
                output=f"test {i}",
            )
            buffer.add(event)

        assert len(buffer) == 2

    def test_get_recent(self) -> None:
        """Test getting recent events."""
        buffer = FalcoEventBuffer()

        for i in range(5):
            event = SecurityEvent(
                uuid=f"test-{i}",
                timestamp=datetime.now(UTC),
                rule="test",
                priority=FalcoPriority.WARNING,
                output=f"test {i}",
            )
            buffer.add(event)

        recent = buffer.get_recent(count=3)
        assert len(recent) == 3
        # Most recent first
        assert recent[0].uuid == "test-4"

    def test_get_recent_with_priority_filter(self) -> None:
        """Test filtering events by priority."""
        buffer = FalcoEventBuffer()

        buffer.add(SecurityEvent(
            uuid="low",
            timestamp=datetime.now(UTC),
            rule="test",
            priority=FalcoPriority.DEBUG,
            output="debug",
        ))
        buffer.add(SecurityEvent(
            uuid="high",
            timestamp=datetime.now(UTC),
            rule="test",
            priority=FalcoPriority.CRITICAL,
            output="critical",
        ))

        recent = buffer.get_recent(min_priority=FalcoPriority.WARNING)
        assert len(recent) == 1
        assert recent[0].uuid == "high"

    def test_get_recent_with_container_filter(self) -> None:
        """Test filtering events by container."""
        buffer = FalcoEventBuffer()

        buffer.add(SecurityEvent(
            uuid="match",
            timestamp=datetime.now(UTC),
            rule="test",
            priority=FalcoPriority.WARNING,
            output="test",
            container=ContainerInfo(name="my-app"),
        ))
        buffer.add(SecurityEvent(
            uuid="no-match",
            timestamp=datetime.now(UTC),
            rule="test",
            priority=FalcoPriority.WARNING,
            output="test",
            container=ContainerInfo(name="other-app"),
        ))

        recent = buffer.get_recent(container="my-.*")
        assert len(recent) == 1
        assert recent[0].uuid == "match"

    def test_get_summary(self) -> None:
        """Test getting event summary."""
        buffer = FalcoEventBuffer()

        buffer.add(SecurityEvent(
            uuid="1",
            timestamp=datetime.now(UTC),
            rule="rule_a",
            priority=FalcoPriority.WARNING,
            output="test",
        ))
        buffer.add(SecurityEvent(
            uuid="2",
            timestamp=datetime.now(UTC),
            rule="rule_a",
            priority=FalcoPriority.WARNING,
            output="test",
        ))
        buffer.add(SecurityEvent(
            uuid="3",
            timestamp=datetime.now(UTC),
            rule="rule_b",
            priority=FalcoPriority.CRITICAL,
            output="test",
        ))

        summary = buffer.get_summary()
        assert summary.total_events == 3
        assert summary.events_by_rule["rule_a"] == 2
        assert summary.events_by_rule["rule_b"] == 1
        assert summary.events_by_priority["Warning"] == 2
        assert summary.events_by_priority["Critical"] == 1

    def test_clear(self) -> None:
        """Test clearing buffer."""
        buffer = FalcoEventBuffer()
        buffer.add(SecurityEvent(
            uuid="test",
            timestamp=datetime.now(UTC),
            rule="test",
            priority=FalcoPriority.WARNING,
            output="test",
        ))
        assert len(buffer) == 1

        buffer.clear()
        assert len(buffer) == 0


# =============================================================================
# FalcoInstaller Tests
# =============================================================================


class TestFalcoInstaller:
    """Tests for FalcoInstaller."""

    def test_detect_package_manager_unknown(self) -> None:
        """Test detecting package manager on unknown system raises error."""
        with patch("shutil.which", return_value=None):
            installer = FalcoInstaller()
            with pytest.raises(UnsupportedDistroError):
                installer.detect_package_manager()

    def test_detect_package_manager_apt(self) -> None:
        """Test detecting apt package manager."""
        def mock_which(cmd: str) -> str | None:
            return "/usr/bin/apt-get" if cmd == "apt-get" else None

        with patch("shutil.which", side_effect=mock_which):
            installer = FalcoInstaller()
            pm = installer.detect_package_manager()
            assert pm == PackageManager.APT

    def test_detect_package_manager_dnf(self) -> None:
        """Test detecting dnf package manager."""
        def mock_which(cmd: str) -> str | None:
            return "/usr/bin/dnf" if cmd == "dnf" else None

        with patch("shutil.which", side_effect=mock_which):
            installer = FalcoInstaller()
            pm = installer.detect_package_manager()
            assert pm == PackageManager.DNF

    def test_is_falco_installed_true(self) -> None:
        """Test checking if Falco is installed."""
        with patch("shutil.which", return_value="/usr/bin/falco"):
            installer = FalcoInstaller()
            assert installer.is_falco_installed() is True

    def test_is_falco_installed_false(self) -> None:
        """Test checking when Falco is not installed."""
        with patch("shutil.which", return_value=None):
            installer = FalcoInstaller()
            assert installer.is_falco_installed() is False

    @patch("subprocess.run")
    def test_get_installed_version(self, mock_run: MagicMock) -> None:
        """Test getting installed Falco version."""
        mock_run.return_value = MagicMock(
            returncode=0,
            stdout="Falco 0.37.1\nDriver version: abc123",
        )
        with patch("shutil.which", return_value="/usr/bin/falco"):
            installer = FalcoInstaller()
            version = installer.get_installed_version()
            assert version == "0.37.1"

    def test_install_unsupported_distro(self) -> None:
        """Test install raises on unsupported distro."""
        with (
            patch("shutil.which", return_value=None),
            patch("os.geteuid", return_value=0),  # Pretend we're root
        ):
            installer = FalcoInstaller()
            with pytest.raises(UnsupportedDistroError):
                installer.install()

    def test_get_status(self) -> None:
        """Test getting Falco status."""
        with patch("shutil.which", return_value=None):
            installer = FalcoInstaller()
            status = installer.get_status()
            assert status.installed is False


# =============================================================================
# RuleManager Tests
# =============================================================================


class TestRuleManager:
    """Tests for RuleManager."""

    def test_rule_manager_creation(self) -> None:
        """Test creating rule manager with custom dirs."""
        with tempfile.TemporaryDirectory() as tmpdir:
            rules_dir = Path(tmpdir) / "rules.d"
            gaol_dir = rules_dir / "gaol"

            manager = RuleManager(
                rules_dir=rules_dir,
                gaol_dir=gaol_dir,
            )
            assert manager.rules_dir == rules_dir
            assert manager.gaol_dir == gaol_dir

    def test_create_rule(self) -> None:
        """Test creating a new rule."""
        with tempfile.TemporaryDirectory() as tmpdir:
            manager = RuleManager(
                rules_dir=Path(tmpdir),
                gaol_dir=Path(tmpdir) / "gaol",
            )

            rule = SecurityRule(
                name="test_rule",
                condition="container",
                output="test output",
                priority=FalcoPriority.WARNING,
            )

            path = manager.create_rule(rule)
            assert path.exists()
            assert "test_rule" in path.name

    def test_create_rule_already_exists(self) -> None:
        """Test creating duplicate rule raises error."""
        with tempfile.TemporaryDirectory() as tmpdir:
            manager = RuleManager(
                rules_dir=Path(tmpdir),
                gaol_dir=Path(tmpdir) / "gaol",
            )

            rule = SecurityRule(
                name="test_rule",
                condition="container",
                output="test",
                priority=FalcoPriority.WARNING,
            )

            manager.create_rule(rule)

            with pytest.raises(RuleExistsError):
                manager.create_rule(rule)

    def test_get_rule(self) -> None:
        """Test getting a rule by name."""
        with tempfile.TemporaryDirectory() as tmpdir:
            manager = RuleManager(
                rules_dir=Path(tmpdir),
                gaol_dir=Path(tmpdir) / "gaol",
            )

            rule = SecurityRule(
                name="test_rule",
                description="Test description",
                condition="container",
                output="test",
                priority=FalcoPriority.WARNING,
            )
            manager.create_rule(rule)

            retrieved = manager.get_rule("test_rule")
            assert retrieved.name == "test_rule"
            assert retrieved.description == "Test description"

    def test_get_rule_not_found(self) -> None:
        """Test getting non-existent rule raises error."""
        with tempfile.TemporaryDirectory() as tmpdir:
            manager = RuleManager(
                rules_dir=Path(tmpdir),
                gaol_dir=Path(tmpdir) / "gaol",
            )

            with pytest.raises(RuleNotFoundError):
                manager.get_rule("nonexistent")

    def test_list_rules(self) -> None:
        """Test listing rules."""
        with tempfile.TemporaryDirectory() as tmpdir:
            manager = RuleManager(
                rules_dir=Path(tmpdir),
                gaol_dir=Path(tmpdir) / "gaol",
            )

            for i in range(3):
                rule = SecurityRule(
                    name=f"rule_{i}",
                    condition="container",
                    output="test",
                    priority=FalcoPriority.WARNING,
                )
                manager.create_rule(rule)

            rules = manager.list_rules(include_builtin=False)
            assert len(rules) == 3

    def test_delete_rule(self) -> None:
        """Test deleting a rule."""
        with tempfile.TemporaryDirectory() as tmpdir:
            manager = RuleManager(
                rules_dir=Path(tmpdir),
                gaol_dir=Path(tmpdir) / "gaol",
            )

            rule = SecurityRule(
                name="test_rule",
                condition="container",
                output="test",
                priority=FalcoPriority.WARNING,
            )
            manager.create_rule(rule)

            manager.delete_rule("test_rule")

            with pytest.raises(RuleNotFoundError):
                manager.get_rule("test_rule")

    def test_enable_disable_rule(self) -> None:
        """Test enabling and disabling rules."""
        with tempfile.TemporaryDirectory() as tmpdir:
            manager = RuleManager(
                rules_dir=Path(tmpdir),
                gaol_dir=Path(tmpdir) / "gaol",
            )

            rule = SecurityRule(
                name="test_rule",
                condition="container",
                output="test",
                priority=FalcoPriority.WARNING,
                enabled=True,
            )
            manager.create_rule(rule)

            manager.disable_rule("test_rule")
            updated = manager.get_rule("test_rule")
            assert updated.enabled is False

            manager.enable_rule("test_rule")
            updated = manager.get_rule("test_rule")
            assert updated.enabled is True

    def test_validate_rule_valid(self) -> None:
        """Test validating a valid rule."""
        manager = RuleManager()
        rule = SecurityRule(
            name="valid_rule",
            condition="container and proc.name=bash",
            output="Shell spawned",
            priority=FalcoPriority.WARNING,
        )
        errors = manager.validate_rule(rule)
        assert len(errors) == 0

    def test_validate_rule_missing_condition_and_output(self) -> None:
        """Test validating rule with empty condition and output."""
        manager = RuleManager()
        # Note: Pydantic validates name format at model creation
        # So we test with a valid name but empty condition/output
        rule = SecurityRule(
            name="valid_name",
            condition="",
            output="",
            priority=FalcoPriority.WARNING,
        )
        errors = manager.validate_rule(rule)
        assert "Rule condition is required" in errors
        assert "Rule output is required" in errors

    def test_validate_rule_invalid_name_raises_at_creation(self) -> None:
        """Test that invalid name raises Pydantic validation error."""
        from pydantic import ValidationError

        # Pydantic validates name pattern at model creation
        with pytest.raises(ValidationError):
            SecurityRule(
                name="invalid-name-with-dashes",
                condition="container",
                output="test",
                priority=FalcoPriority.WARNING,
            )

    def test_validate_rule_unbalanced_parens(self) -> None:
        """Test validating rule with unbalanced parentheses."""
        manager = RuleManager()
        rule = SecurityRule(
            name="test_rule",
            condition="(container and (proc.name=bash)",
            output="test",
            priority=FalcoPriority.WARNING,
        )
        errors = manager.validate_rule(rule)
        assert any("parentheses" in e.lower() for e in errors)

    def test_import_export_rules(self) -> None:
        """Test importing and exporting rules."""
        with tempfile.TemporaryDirectory() as tmpdir:
            manager = RuleManager(
                rules_dir=Path(tmpdir),
                gaol_dir=Path(tmpdir) / "gaol",
            )

            # Create some rules
            for i in range(2):
                rule = SecurityRule(
                    name=f"rule_{i}",
                    condition="container",
                    output="test",
                    priority=FalcoPriority.WARNING,
                )
                manager.create_rule(rule)

            # Export
            export_path = Path(tmpdir) / "exported.yaml"
            count = manager.export_rules(export_path)
            assert count == 2
            assert export_path.exists()

            # Create new manager and import
            manager2 = RuleManager(
                rules_dir=Path(tmpdir) / "new",
                gaol_dir=Path(tmpdir) / "new" / "gaol",
            )
            imported = manager2.import_rules(export_path)
            assert imported == 2


# =============================================================================
# SeccompProfileGenerator Tests
# =============================================================================


class TestSeccompProfileGenerator:
    """Tests for SeccompProfileGenerator."""

    def test_generator_creation(self) -> None:
        """Test creating generator with custom dir."""
        with tempfile.TemporaryDirectory() as tmpdir:
            generator = SeccompProfileGenerator(profiles_dir=Path(tmpdir))
            assert generator.profiles_dir == Path(tmpdir)

    def test_generate_default(self) -> None:
        """Test generating default profile."""
        generator = SeccompProfileGenerator()
        profile = generator.generate_default("test-container")

        assert profile.name == "test-container"
        assert profile.defaultAction == SeccompAction.SCMP_ACT_ERRNO
        assert len(profile.syscalls) > 0
        assert profile.generated_from == "gaol-default"

    def test_generate_restrictive(self) -> None:
        """Test generating restrictive profile."""
        generator = SeccompProfileGenerator()
        profile = generator.generate_restrictive("test-container")

        assert profile.name == "test-container"
        assert profile.defaultAction == SeccompAction.SCMP_ACT_KILL_PROCESS
        assert profile.generated_from == "gaol-restrictive"

    def test_save_and_load_profile(self) -> None:
        """Test saving and loading a profile."""
        with tempfile.TemporaryDirectory() as tmpdir:
            generator = SeccompProfileGenerator(profiles_dir=Path(tmpdir))

            profile = generator.generate_default("test")
            saved_path = generator.save_profile(profile)

            assert saved_path.exists()

            loaded = generator.load_profile(saved_path)
            assert loaded.name == "test"
            assert loaded.defaultAction == SeccompAction.SCMP_ACT_ERRNO

    def test_load_profile_not_found(self) -> None:
        """Test loading non-existent profile raises error."""
        generator = SeccompProfileGenerator()

        with pytest.raises(FileNotFoundError):
            generator.load_profile(Path("/nonexistent/profile.json"))

    def test_list_profiles(self) -> None:
        """Test listing saved profiles."""
        with tempfile.TemporaryDirectory() as tmpdir:
            generator = SeccompProfileGenerator(profiles_dir=Path(tmpdir))

            # Initially empty
            assert len(generator.list_profiles()) == 0

            # Save some profiles
            for i in range(3):
                profile = generator.generate_default(f"profile-{i}")
                generator.save_profile(profile)

            profiles = generator.list_profiles()
            assert len(profiles) == 3

    def test_baseline_syscalls_not_empty(self) -> None:
        """Test that baseline syscalls list is populated."""
        assert len(BASELINE_SYSCALLS) > 50
        assert "read" in BASELINE_SYSCALLS
        assert "write" in BASELINE_SYSCALLS
        assert "open" in BASELINE_SYSCALLS


# =============================================================================
# AppArmorProfileGenerator Tests
# =============================================================================


class TestAppArmorProfileGenerator:
    """Tests for AppArmorProfileGenerator."""

    def test_generator_creation(self) -> None:
        """Test creating generator."""
        with tempfile.TemporaryDirectory() as tmpdir:
            generator = AppArmorProfileGenerator(profiles_dir=Path(tmpdir))
            assert generator.profiles_dir == Path(tmpdir)

    def test_generate_default(self) -> None:
        """Test generating default profile."""
        generator = AppArmorProfileGenerator()
        profile = generator.generate_default("my-container")

        assert profile.name == "my-container"
        assert profile.mode == AppArmorMode.ENFORCE
        assert len(profile.capabilities_allow) > 0
        assert len(profile.file_read) > 0

    def test_generate_restrictive(self) -> None:
        """Test generating restrictive profile."""
        generator = AppArmorProfileGenerator()
        profile = generator.generate_restrictive("my-container")

        assert profile.name == "my-container"
        # Restrictive profile should have fewer capabilities
        default_profile = generator.generate_default("other")
        assert len(profile.capabilities_allow) < len(default_profile.capabilities_allow)

    def test_sanitize_profile_name(self) -> None:
        """Test profile name sanitization."""
        generator = AppArmorProfileGenerator()

        assert generator._sanitize_profile_name("valid_name") == "valid_name"
        assert generator._sanitize_profile_name("name-with-dashes") == "name-with-dashes"
        assert generator._sanitize_profile_name("name with spaces") == "name_with_spaces"
        # Names starting with numbers get prefixed
        assert generator._sanitize_profile_name("123start") == "gaol_123start"

    def test_save_and_load_profile(self) -> None:
        """Test saving and loading profile."""
        with tempfile.TemporaryDirectory() as tmpdir:
            generator = AppArmorProfileGenerator(profiles_dir=Path(tmpdir))

            profile = generator.generate_default("test")
            saved_path = generator.save_profile(profile)

            assert saved_path.exists()
            content = saved_path.read_text()
            assert "profile test" in content

    def test_list_profiles(self) -> None:
        """Test listing profiles."""
        with tempfile.TemporaryDirectory() as tmpdir:
            generator = AppArmorProfileGenerator(profiles_dir=Path(tmpdir))

            # Initially empty
            assert len(generator.list_profiles()) == 0

            # Save some profiles
            for i in range(2):
                profile = generator.generate_default(f"profile_{i}")
                generator.save_profile(profile)

            profiles = generator.list_profiles()
            assert len(profiles) == 2

    def test_default_capabilities_defined(self) -> None:
        """Test that default capabilities are defined."""
        assert len(DEFAULT_CAPABILITIES_ALLOW) > 0
        assert "chown" in DEFAULT_CAPABILITIES_ALLOW
        assert len(DEFAULT_CAPABILITIES_DENY) > 0
        assert "sys_admin" in DEFAULT_CAPABILITIES_DENY

    def test_default_file_patterns_defined(self) -> None:
        """Test that default file patterns are defined."""
        assert len(DEFAULT_FILE_READ) > 0
        assert len(DEFAULT_FILE_DENY) > 0
        assert "/etc/shadow" in DEFAULT_FILE_DENY


# =============================================================================
# Integration Tests
# =============================================================================


class TestSecurityIntegration:
    """Integration tests for the security module."""

    def test_rule_roundtrip(self) -> None:
        """Test full rule creation and retrieval cycle."""
        with tempfile.TemporaryDirectory() as tmpdir:
            manager = RuleManager(
                rules_dir=Path(tmpdir),
                gaol_dir=Path(tmpdir) / "gaol",
            )

            # Create rule
            rule = SecurityRule(
                name="integration_test_rule",
                description="Integration test rule",
                condition="container and proc.name=bash",
                output="Shell spawned (user=%user.name container=%container.name)",
                priority=FalcoPriority.WARNING,
                tags=["container", "shell", "test"],
            )
            manager.create_rule(rule)

            # Retrieve and verify
            retrieved = manager.get_rule("integration_test_rule")
            assert retrieved.description == "Integration test rule"
            assert retrieved.priority == FalcoPriority.WARNING
            assert "container" in retrieved.tags

            # Update via disable/enable
            manager.disable_rule("integration_test_rule")
            updated = manager.get_rule("integration_test_rule")
            assert updated.enabled is False

            # Delete
            manager.delete_rule("integration_test_rule")
            assert len(manager.list_rules(include_builtin=False)) == 0

    def test_seccomp_profile_oci_compliance(self) -> None:
        """Test that generated seccomp profiles are OCI-compliant."""
        generator = SeccompProfileGenerator()
        profile = generator.generate_default("test")
        oci_json = profile.to_oci_json()

        # Check required OCI fields
        assert "defaultAction" in oci_json
        assert "architectures" in oci_json
        assert "syscalls" in oci_json

        # Validate JSON serialization
        json_str = json.dumps(oci_json)
        parsed = json.loads(json_str)
        assert parsed["defaultAction"] == "SCMP_ACT_ERRNO"

    def test_apparmor_profile_syntax(self) -> None:
        """Test that generated AppArmor profiles have valid syntax."""
        generator = AppArmorProfileGenerator()
        profile = generator.generate_default("test_container")
        text = profile.to_apparmor_text()

        # Check required syntax elements
        assert text.startswith("#")  # Comment header
        assert "profile test_container" in text
        assert "{" in text
        assert "}" in text
        assert "#include <abstractions/base>" in text

    def test_event_buffer_thread_safety(self) -> None:
        """Test event buffer is thread-safe."""
        import threading

        buffer = FalcoEventBuffer(max_size=1000)
        errors: list[Exception] = []

        def add_events(start: int, count: int) -> None:
            try:
                for i in range(count):
                    event = SecurityEvent(
                        uuid=f"thread-{start}-{i}",
                        timestamp=datetime.now(UTC),
                        rule="test",
                        priority=FalcoPriority.WARNING,
                        output="test",
                    )
                    buffer.add(event)
            except Exception as e:
                errors.append(e)

        threads = [
            threading.Thread(target=add_events, args=(i * 100, 100))
            for i in range(5)
        ]

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0
        assert len(buffer) == 500
