"""Integration tests: Daemon with capability-based policy validation."""

import json
import struct
import asyncio
from unittest.mock import AsyncMock, MagicMock
import pytest

from rpipe.daemon import RPipeDaemon
from rpipe.protocol import (
    pack_frame,
    read_frame,
    TYPE_RUN_REQ,
    TYPE_META,
    TYPE_STDOUT,
    TYPE_EXIT,
    TYPE_ERROR,
)
from rpipe.capability import (
    Capability,
    CapabilitySet,
    OperationType,
)
from rpipe.ssh_backend import SSHPool, RunHandle


class TestDaemonWithCapabilityPolicy:
    """Integration tests for daemon with capability-based policy."""

    @pytest.fixture
    def daemon_with_policy(self):
        """Daemon configured with capability policy."""
        pool = SSHPool(known_hosts=None)
        daemon = RPipeDaemon("test.sock", ssh_pool=pool)
        return daemon

    @pytest.mark.asyncio
    async def test_daemon_accepts_command_with_policy(self, daemon_with_policy):
        """Daemon should accept command when policy permits it."""
        reader = asyncio.StreamReader()
        writer = AsyncMock()

        # RUN_REQ with policy="read_only"
        req = {
            "host": "example.com",
            "cmd": "cat /var/log/syslog",
            "policy": "read_only",
        }
        frame = pack_frame(TYPE_RUN_REQ, 0, json.dumps(req).encode("utf-8"))
        reader.feed_data(frame)

        # Mock SSH backend to return immediately
        mock_proc = AsyncMock()
        mock_proc.stdout = AsyncMock()
        mock_proc.stderr = AsyncMock()
        async def mock_read(size):
            return b""
        mock_proc.stdout.read = mock_read
        mock_proc.stderr.read = mock_read

        mock_run = AsyncMock(spec=RunHandle)
        mock_run.proc = mock_proc
        mock_run.wait = AsyncMock(return_value=0)

        mock_backend = AsyncMock()
        mock_backend.run = AsyncMock(return_value=mock_run)
        daemon_with_policy.backend = mock_backend

        reader.feed_eof()

        # Handle should not reject the command
        await daemon_with_policy.handle_client(reader, writer)

        # Should have sent META and EXIT frames (not ERROR for policy)
        assert writer.write.called
        first_frame_call = writer.write.call_args_list[0][0][0]
        assert first_frame_call[4] == TYPE_META  # First frame should be META, not ERROR

    @pytest.mark.asyncio
    async def test_daemon_rejects_command_with_policy(self, daemon_with_policy):
        """Daemon should reject command when policy denies it."""
        reader = asyncio.StreamReader()
        writer = AsyncMock()

        # RUN_REQ with restrictive policy trying to use cat (not in preset)
        req = {
            "host": "example.com",
            "cmd": "cat /var/log/syslog",
            "policy": "restrictive",
        }
        frame = pack_frame(TYPE_RUN_REQ, 0, json.dumps(req).encode("utf-8"))
        reader.feed_data(frame)
        reader.feed_eof()

        # Handle should reject the command
        await daemon_with_policy.handle_client(reader, writer)

        # Should have sent ERROR frame
        assert writer.write.called
        error_frame = writer.write.call_args_list[0][0][0]
        assert error_frame[4] == TYPE_ERROR

    @pytest.mark.asyncio
    async def test_daemon_unknown_policy_raises_error(self, daemon_with_policy):
        """Daemon should error on unknown policy name."""
        reader = asyncio.StreamReader()
        writer = AsyncMock()

        # RUN_REQ with unknown policy
        req = {
            "host": "example.com",
            "cmd": "echo hello",
            "policy": "unknown_policy",
        }
        frame = pack_frame(TYPE_RUN_REQ, 0, json.dumps(req).encode("utf-8"))
        reader.feed_data(frame)
        reader.feed_eof()

        await daemon_with_policy.handle_client(reader, writer)

        # Should have sent ERROR frame
        assert writer.write.called
        error_frame = writer.write.call_args_list[0][0][0]
        assert error_frame[4] == TYPE_ERROR

    @pytest.mark.asyncio
    async def test_daemon_falls_back_to_old_policy(self, daemon_with_policy):
        """Daemon should use old token-based validation if policy not specified."""
        reader = asyncio.StreamReader()
        writer = AsyncMock()

        # RUN_REQ without policy field (old style)
        req = {
            "host": "example.com",
            "cmd": "echo hello",
            "read_only": True,
        }
        frame = pack_frame(TYPE_RUN_REQ, 0, json.dumps(req).encode("utf-8"))
        reader.feed_data(frame)

        # Mock SSH backend
        mock_proc = AsyncMock()
        mock_proc.stdout = AsyncMock()
        mock_proc.stderr = AsyncMock()
        async def mock_read(size):
            return b""
        mock_proc.stdout.read = mock_read
        mock_proc.stderr.read = mock_read

        mock_run = AsyncMock(spec=RunHandle)
        mock_run.proc = mock_proc
        mock_run.wait = AsyncMock(return_value=0)

        mock_backend = AsyncMock()
        mock_backend.run = AsyncMock(return_value=mock_run)
        daemon_with_policy.backend = mock_backend

        reader.feed_eof()

        await daemon_with_policy.handle_client(reader, writer)

        # Should use old token-based validation and succeed
        assert writer.write.called
        first_frame = writer.write.call_args_list[0][0][0]
        assert first_frame[4] == TYPE_META  # Should accept the command


class TestConcurrentClientHandling:
    """Test concurrent client handling."""

    @pytest.mark.asyncio
    async def test_load_policy_read_only(self):
        """_load_policy should return read_only preset."""
        pool = SSHPool(known_hosts=None)
        daemon = RPipeDaemon("test.sock", ssh_pool=pool)

        policy = daemon._load_policy("read_only")
        assert policy is not None
        assert policy.permits(OperationType.EXECUTE_BINARY, "cat")
        assert not policy.permits(OperationType.EXECUTE_BINARY, "rm")

    @pytest.mark.asyncio
    async def test_load_policy_write_capable(self):
        """_load_policy should return write_capable preset."""
        pool = SSHPool(known_hosts=None)
        daemon = RPipeDaemon("test.sock", ssh_pool=pool)

        policy = daemon._load_policy("write_capable")
        assert policy is not None
        assert policy.permits(OperationType.EXECUTE_BINARY, "cat")
        assert policy.permits(OperationType.WRITE_FILE, "/tmp/file")

    @pytest.mark.asyncio
    async def test_load_policy_restrictive(self):
        """_load_policy should return restrictive preset."""
        pool = SSHPool(known_hosts=None)
        daemon = RPipeDaemon("test.sock", ssh_pool=pool)

        policy = daemon._load_policy("restrictive")
        assert policy is not None
        assert policy.permits(OperationType.EXECUTE_BINARY, "echo")
        assert not policy.permits(OperationType.EXECUTE_BINARY, "cat")

    @pytest.mark.asyncio
    async def test_load_policy_default(self):
        """_load_policy with None should return read_only."""
        pool = SSHPool(known_hosts=None)
        daemon = RPipeDaemon("test.sock", ssh_pool=pool)

        policy = daemon._load_policy(None)
        assert policy is not None
        assert policy.permits(OperationType.EXECUTE_BINARY, "cat")

    @pytest.mark.asyncio
    async def test_load_policy_unknown_raises(self):
        """_load_policy with unknown policy should raise."""
        pool = SSHPool(known_hosts=None)
        daemon = RPipeDaemon("test.sock", ssh_pool=pool)

        from rpipe.daemon import DaemonError
        with pytest.raises(DaemonError):
            daemon._load_policy("unknown")


class TestPolicyIntegrationWithCommandParser:
    """Test that policy works correctly with parsed commands."""

    @pytest.mark.asyncio
    async def test_restrictive_policy_blocks_dangerous_binaries(self):
        """Restrictive policy should block cat, grep, etc."""
        from rpipe.policy_capabilities import validate_command_against_capabilities

        pool = SSHPool(known_hosts=None)
        daemon = RPipeDaemon("test.sock", ssh_pool=pool)

        policy = daemon._load_policy("restrictive")

        # These should fail in restrictive policy
        with pytest.raises(Exception):  # PolicyError
            validate_command_against_capabilities("cat /var/log/syslog", policy)

        with pytest.raises(Exception):
            validate_command_against_capabilities("grep error /var/log/syslog", policy)

    @pytest.mark.asyncio
    async def test_read_only_policy_allows_safe_commands(self):
        """Read-only policy should allow cat, grep, ls, etc."""
        from rpipe.policy_capabilities import validate_command_against_capabilities

        pool = SSHPool(known_hosts=None)
        daemon = RPipeDaemon("test.sock", ssh_pool=pool)

        policy = daemon._load_policy("read_only")

        # These should succeed in read-only policy
        validate_command_against_capabilities("cat /var/log/syslog", policy)
        validate_command_against_capabilities("grep error /var/log/syslog", policy)
        validate_command_against_capabilities("ls -la /var/log", policy)

    @pytest.mark.asyncio
    async def test_read_only_policy_blocks_writes(self):
        """Read-only policy should block write operations."""
        from rpipe.policy_capabilities import validate_command_against_capabilities

        pool = SSHPool(known_hosts=None)
        daemon = RPipeDaemon("test.sock", ssh_pool=pool)

        policy = daemon._load_policy("read_only")

        # This should fail in read-only policy
        with pytest.raises(Exception):  # PolicyError
            validate_command_against_capabilities("echo hello > /tmp/file", policy)
