"""Tests for capability-based command validation model."""

import pytest

from rpipe.capability import (
    Capability,
    CapabilitySet,
    OperationType,
    PolicyError,
    DEFAULT_READ_ONLY_CAPABILITIES,
    DEFAULT_WRITE_CAPABLE_CAPABILITIES,
    DEFAULT_RESTRICTIVE_CAPABILITIES,
)


class TestCapabilityMatching:
    """Test basic capability matching logic."""

    def test_exact_match(self):
        """Exact resource match should succeed."""
        cap = Capability(OperationType.READ_FILE, "/var/log/syslog")
        assert cap.matches(OperationType.READ_FILE, "/var/log/syslog")

    def test_operation_mismatch(self):
        """Different operation should not match."""
        cap = Capability(OperationType.READ_FILE, "/var/log/syslog")
        assert not cap.matches(OperationType.WRITE_FILE, "/var/log/syslog")

    def test_resource_mismatch(self):
        """Different resource should not match."""
        cap = Capability(OperationType.READ_FILE, "/var/log/syslog")
        assert not cap.matches(OperationType.READ_FILE, "/var/log/auth.log")

    def test_no_pattern_matches_all(self):
        """None pattern should match all resources."""
        cap = Capability(OperationType.WRITE_STDOUT, None)
        assert cap.matches(OperationType.WRITE_STDOUT, "anything")
        assert cap.matches(OperationType.WRITE_STDOUT, "/arbitrary/path")
        assert cap.matches(OperationType.WRITE_STDOUT, "")

    def test_glob_pattern_single_star(self):
        """Single * glob pattern should match files in directory."""
        cap = Capability(OperationType.READ_FILE, "/var/log/*")
        assert cap.matches(OperationType.READ_FILE, "/var/log/syslog")
        assert cap.matches(OperationType.READ_FILE, "/var/log/auth.log")
        assert cap.matches(OperationType.READ_FILE, "/var/log/kernel.log")
        assert not cap.matches(OperationType.READ_FILE, "/var/log/subdir/file")

    def test_glob_pattern_double_star(self):
        """Double ** glob pattern should match recursively."""
        cap = Capability(OperationType.READ_FILE, "/var/log/**/*")
        assert cap.matches(OperationType.READ_FILE, "/var/log/syslog")
        assert cap.matches(OperationType.READ_FILE, "/var/log/apache2/access.log")
        assert cap.matches(OperationType.READ_FILE, "/var/log/deep/nested/file.log")
        assert not cap.matches(OperationType.READ_FILE, "/var/log2/file")

    def test_glob_pattern_question_mark(self):
        """? glob pattern should match single character."""
        cap = Capability(OperationType.READ_FILE, "/tmp/file?.txt")
        assert cap.matches(OperationType.READ_FILE, "/tmp/file1.txt")
        assert cap.matches(OperationType.READ_FILE, "/tmp/fileA.txt")
        assert not cap.matches(OperationType.READ_FILE, "/tmp/file12.txt")
        assert not cap.matches(OperationType.READ_FILE, "/tmp/file.txt")

    def test_glob_pattern_character_class(self):
        """[...] character class should work."""
        cap = Capability(OperationType.READ_FILE, "/var/log/[aA]uth.log")
        assert cap.matches(OperationType.READ_FILE, "/var/log/auth.log")
        assert cap.matches(OperationType.READ_FILE, "/var/log/Auth.log")
        assert not cap.matches(OperationType.READ_FILE, "/var/log/other.log")

    def test_binary_pattern_with_pipe(self):
        """Pipe-separated binary names should work as alternatives."""
        cap = Capability(OperationType.EXECUTE_BINARY, "cat|grep|ls")
        assert cap.matches(OperationType.EXECUTE_BINARY, "cat")
        assert cap.matches(OperationType.EXECUTE_BINARY, "grep")
        assert cap.matches(OperationType.EXECUTE_BINARY, "ls")
        assert not cap.matches(OperationType.EXECUTE_BINARY, "rm")

    def test_case_sensitive_matching(self):
        """Matching should be case-sensitive."""
        cap = Capability(OperationType.READ_FILE, "/var/log/Syslog")
        assert cap.matches(OperationType.READ_FILE, "/var/log/Syslog")
        assert not cap.matches(OperationType.READ_FILE, "/var/log/syslog")


class TestCapabilitySet:
    """Test CapabilitySet permission checking."""

    def test_permits_with_matching_capability(self):
        """Should permit operation if capability exists."""
        cap_set = CapabilitySet([
            Capability(OperationType.READ_FILE, "/var/log/*"),
        ])
        assert cap_set.permits(OperationType.READ_FILE, "/var/log/syslog")

    def test_denies_without_matching_capability(self):
        """Should deny operation if no matching capability."""
        cap_set = CapabilitySet([
            Capability(OperationType.READ_FILE, "/var/log/*"),
        ])
        assert not cap_set.permits(OperationType.WRITE_FILE, "/tmp/file")

    def test_empty_capability_set_denies_all(self):
        """Empty capability set should deny all operations."""
        cap_set = CapabilitySet([])
        assert not cap_set.permits(OperationType.READ_FILE, "/var/log/syslog")
        assert not cap_set.permits(OperationType.EXECUTE_BINARY, "cat")

    def test_multiple_capabilities_any_match(self):
        """Should permit if ANY capability matches."""
        cap_set = CapabilitySet([
            Capability(OperationType.READ_FILE, "/var/log/*"),
            Capability(OperationType.READ_FILE, "/tmp/*"),
            Capability(OperationType.READ_FILE, "/home/**/*"),
        ])
        assert cap_set.permits(OperationType.READ_FILE, "/var/log/syslog")
        assert cap_set.permits(OperationType.READ_FILE, "/tmp/file")
        assert cap_set.permits(OperationType.READ_FILE, "/home/user/doc")
        assert not cap_set.permits(OperationType.READ_FILE, "/etc/passwd")

    def test_no_pattern_capability_allows_all(self):
        """Capability with no pattern should allow all resources."""
        cap_set = CapabilitySet([
            Capability(OperationType.WRITE_STDOUT, None),
        ])
        assert cap_set.permits(OperationType.WRITE_STDOUT, "/any/file")
        assert cap_set.permits(OperationType.WRITE_STDOUT, "any string")
        assert cap_set.permits(OperationType.WRITE_STDOUT, "")


class TestCapabilitySetCheckAllowed:
    """Test check_allowed method that raises PolicyError."""

    def test_check_allowed_succeeds(self):
        """check_allowed should not raise if permitted."""
        cap_set = CapabilitySet([
            Capability(OperationType.READ_FILE, "/var/log/*"),
        ])
        cap_set.check_allowed(OperationType.READ_FILE, "/var/log/syslog")
        # No exception raised

    def test_check_allowed_raises_on_deny(self):
        """check_allowed should raise PolicyError if denied."""
        cap_set = CapabilitySet([
            Capability(OperationType.READ_FILE, "/var/log/*"),
        ])
        with pytest.raises(PolicyError):
            cap_set.check_allowed(OperationType.WRITE_FILE, "/tmp/file")

    def test_error_message_includes_operation(self):
        """Error message should name the operation."""
        cap_set = CapabilitySet([
            Capability(OperationType.READ_FILE, "/var/log/*"),
        ])
        with pytest.raises(PolicyError) as exc_info:
            cap_set.check_allowed(OperationType.WRITE_FILE, "/tmp/file")
        assert "write_file" in str(exc_info.value)

    def test_error_message_includes_resource(self):
        """Error message should name the resource."""
        cap_set = CapabilitySet([
            Capability(OperationType.READ_FILE, "/var/log/*"),
        ])
        with pytest.raises(PolicyError) as exc_info:
            cap_set.check_allowed(OperationType.WRITE_FILE, "/tmp/file")
        assert "/tmp/file" in str(exc_info.value)

    def test_error_message_lists_capabilities(self):
        """Error message should list available capabilities."""
        cap_set = CapabilitySet([
            Capability(OperationType.READ_FILE, "/var/log/*"),
            Capability(OperationType.EXECUTE_BINARY, "cat|grep"),
        ])
        with pytest.raises(PolicyError) as exc_info:
            cap_set.check_allowed(OperationType.WRITE_FILE, "/tmp/file")
        error_msg = str(exc_info.value)
        assert "read_file" in error_msg or "/var/log/*" in error_msg

    def test_error_message_says_not_permitted(self):
        """Error message should say operation is not permitted."""
        cap_set = CapabilitySet([])
        with pytest.raises(PolicyError) as exc_info:
            cap_set.check_allowed(OperationType.READ_FILE, "/var/log/syslog")
        assert "not permitted" in str(exc_info.value)


class TestPresetCapabilities:
    """Test preset capability sets."""

    def test_read_only_preset_allows_cat(self):
        """Read-only preset should allow cat."""
        cap_set = CapabilitySet(DEFAULT_READ_ONLY_CAPABILITIES)
        assert cap_set.permits(OperationType.EXECUTE_BINARY, "cat")
        assert cap_set.permits(OperationType.READ_FILE, "/var/log/syslog")

    def test_read_only_preset_allows_grep(self):
        """Read-only preset should allow grep."""
        cap_set = CapabilitySet(DEFAULT_READ_ONLY_CAPABILITIES)
        assert cap_set.permits(OperationType.EXECUTE_BINARY, "grep")

    def test_read_only_preset_allows_ls(self):
        """Read-only preset should allow ls."""
        cap_set = CapabilitySet(DEFAULT_READ_ONLY_CAPABILITIES)
        assert cap_set.permits(OperationType.EXECUTE_BINARY, "ls")

    def test_read_only_preset_denies_rm(self):
        """Read-only preset should deny rm."""
        cap_set = CapabilitySet(DEFAULT_READ_ONLY_CAPABILITIES)
        assert not cap_set.permits(OperationType.EXECUTE_BINARY, "rm")

    def test_read_only_preset_denies_write(self):
        """Read-only preset should deny write operations."""
        cap_set = CapabilitySet(DEFAULT_READ_ONLY_CAPABILITIES)
        # Read-only preset doesn't have WRITE_FILE capability
        # (or has it only for restricted paths)
        write_denied = not cap_set.permits(OperationType.WRITE_FILE, "/var/log/file")
        # Either denied or restricted - this is acceptable
        assert write_denied or not cap_set.permits(OperationType.WRITE_FILE, "/etc/passwd")

    def test_write_capable_preset_allows_write_to_tmp(self):
        """Write-capable preset should allow writes to /tmp/*."""
        cap_set = CapabilitySet(DEFAULT_WRITE_CAPABLE_CAPABILITIES)
        assert cap_set.permits(OperationType.WRITE_FILE, "/tmp/file")

    def test_write_capable_preset_denies_write_to_etc(self):
        """Write-capable preset should restrict writes to /etc."""
        cap_set = CapabilitySet(DEFAULT_WRITE_CAPABLE_CAPABILITIES)
        assert not cap_set.permits(OperationType.WRITE_FILE, "/etc/passwd")

    def test_restrictive_preset_allows_minimal_commands(self):
        """Restrictive preset should allow only safe commands."""
        cap_set = CapabilitySet(DEFAULT_RESTRICTIVE_CAPABILITIES)
        assert cap_set.permits(OperationType.EXECUTE_BINARY, "echo")
        assert cap_set.permits(OperationType.EXECUTE_BINARY, "date")
        assert cap_set.permits(OperationType.EXECUTE_BINARY, "hostname")

    def test_restrictive_preset_denies_file_access(self):
        """Restrictive preset should deny file access."""
        cap_set = CapabilitySet(DEFAULT_RESTRICTIVE_CAPABILITIES)
        # Restrictive preset may not have READ_FILE capability
        read_denied = not cap_set.permits(OperationType.READ_FILE, "/var/log/syslog")
        # Should be denied or restricted
        assert read_denied or not cap_set.permits(OperationType.READ_FILE, "/etc/passwd")

    def test_restrictive_preset_denies_cat(self):
        """Restrictive preset should deny cat."""
        cap_set = CapabilitySet(DEFAULT_RESTRICTIVE_CAPABILITIES)
        assert not cap_set.permits(OperationType.EXECUTE_BINARY, "cat")

    def test_restrictive_preset_denies_grep(self):
        """Restrictive preset should deny grep."""
        cap_set = CapabilitySet(DEFAULT_RESTRICTIVE_CAPABILITIES)
        assert not cap_set.permits(OperationType.EXECUTE_BINARY, "grep")


class TestCapabilitySetRepresentation:
    """Test string representation for debugging."""

    def test_repr_shows_count(self):
        """repr should show capability count."""
        cap_set = CapabilitySet([
            Capability(OperationType.READ_FILE, "/var/log/*"),
            Capability(OperationType.EXECUTE_BINARY, "cat"),
        ])
        repr_str = repr(cap_set)
        assert "2 capabilities" in repr_str

    def test_describe_shows_operations(self):
        """_describe_capabilities should list operations."""
        cap_set = CapabilitySet([
            Capability(OperationType.READ_FILE, "/var/log/*"),
            Capability(OperationType.EXECUTE_BINARY, "cat"),
        ])
        desc = cap_set._describe_capabilities()
        assert "read_file" in desc
        assert "execute_binary" in desc

    def test_describe_shows_patterns(self):
        """_describe_capabilities should show patterns."""
        cap_set = CapabilitySet([
            Capability(OperationType.READ_FILE, "/var/log/*"),
        ])
        desc = cap_set._describe_capabilities()
        assert "/var/log/*" in desc

    def test_describe_empty_set(self):
        """_describe_capabilities for empty set should be clear."""
        cap_set = CapabilitySet([])
        desc = cap_set._describe_capabilities()
        assert "no capabilities" in desc.lower() or "" in desc


class TestOperationTypes:
    """Test OperationType enum."""

    def test_read_file_operation(self):
        """READ_FILE operation should exist."""
        assert OperationType.READ_FILE.value == "read_file"

    def test_write_file_operation(self):
        """WRITE_FILE operation should exist."""
        assert OperationType.WRITE_FILE.value == "write_file"

    def test_execute_binary_operation(self):
        """EXECUTE_BINARY operation should exist."""
        assert OperationType.EXECUTE_BINARY.value == "execute_binary"

    def test_all_operations_exist(self):
        """All expected operations should be defined."""
        expected_ops = [
            "read_file",
            "list_dir",
            "write_file",
            "execute_binary",
            "read_stdin",
            "write_stdout",
            "write_stderr",
            "read_env",
            "system_info",
        ]
        for op_name in expected_ops:
            # Check that operation exists
            assert any(op.value == op_name for op in OperationType)
