"""
Tests for Features 9, 10, 11:
  - Permission Engine (4-tier routing)
  - Audit Preview Mode (ActionPreview)
  - Full Audit Log (CRUD, filter, export)

Action type names must match DEFAULT_RULES keys (underscore format):
  SAFE:   memory_read, web_search, chat_response
  NOTIFY: file_read, web_browse, shell_safe
  ASK:    screen_capture, file_write, shell_exec
  BLOCK:  access_credentials, sudo_command, rm_recursive_root, fork_bomb
"""

import json
import pytest
from unittest.mock import AsyncMock


# ── Feature 9: Permission Engine (4-tier routing) ─────────────────────────────

class TestPermissionTiers:
    """Every action type routes to the correct tier."""

    async def test_safe_action_auto_approved_silently(self, permission_engine):
        """SAFE actions are approved without calling any callback."""
        notify_called = []

        async def on_notify(message: str):
            notify_called.append(message)

        permission_engine.set_notify_callback(on_notify)

        result = await permission_engine.check(
            action_type="memory_read",
            description="Read user facts",
            source="test",
            session_id="s1",
        )

        assert result is True
        assert len(notify_called) == 0  # SAFE → silent

    async def test_notify_action_auto_approved_with_callback(self, permission_engine):
        """NOTIFY actions are auto-approved and the notify callback fires."""
        notified = []

        async def on_notify(message: str):
            notified.append(message)

        permission_engine.set_notify_callback(on_notify)

        result = await permission_engine.check(
            action_type="file_read",
            description="Read ~/Documents/notes.txt",
            source="test",
            session_id="s1",
        )

        assert result is True
        assert len(notified) == 1
        assert "notes.txt" in notified[0]

    async def test_ask_action_approved_when_callback_true(self, permission_engine):
        """ASK actions are approved when the approval callback returns True."""
        permission_engine.set_approval_callback(AsyncMock(return_value=True))

        result = await permission_engine.check(
            action_type="shell_exec",
            description="Run: git status",
            source="test",
            session_id="s1",
        )

        assert result is True

    async def test_ask_action_denied_when_callback_false(self, permission_engine):
        """ASK actions are denied when the approval callback returns False."""
        permission_engine.set_approval_callback(AsyncMock(return_value=False))

        result = await permission_engine.check(
            action_type="screen_capture",
            description="Take a screenshot",
            source="test",
            session_id="s1",
        )

        assert result is False

    async def test_block_action_always_denied_regardless_of_callback(self, permission_engine):
        """BLOCK actions cannot be approved — even with a permissive callback."""
        permission_engine.set_approval_callback(AsyncMock(return_value=True))

        result = await permission_engine.check(
            action_type="access_credentials",
            description="Read SSH keys",
            source="test",
            session_id="s1",
        )

        assert result is False

    async def test_all_hardcoded_blocks_cannot_be_unlocked(self, permission_engine):
        """Every entry in HARDCODED_BLOCKS is rejected regardless of callback."""
        from duckclaw.permissions.engine import HARDCODED_BLOCKS

        permission_engine.set_approval_callback(AsyncMock(return_value=True))

        for blocked in HARDCODED_BLOCKS:
            result = await permission_engine.check(
                action_type=blocked,
                description=f"Attempting blocked: {blocked}",
                source="attacker",
                session_id="s1",
            )
            assert result is False, f"Expected {blocked} to be permanently blocked"

    async def test_unknown_action_type_defaults_to_ask(self, permission_engine):
        """Unrecognised action types fall back to ASK (conservative default)."""
        approval_mock = AsyncMock(return_value=True)
        permission_engine.set_approval_callback(approval_mock)

        result = await permission_engine.check(
            action_type="some.completely.unknown.action",
            description="Unknown action",
            source="test",
            session_id="s1",
        )

        approval_mock.assert_called_once()
        assert result is True

    async def test_get_tier_returns_correct_tiers(self, permission_engine):
        """get_tier() returns the right Tier enum for known action types."""
        from duckclaw.permissions.engine import Tier

        assert permission_engine.get_tier("memory_read") == Tier.SAFE
        assert permission_engine.get_tier("web_search") == Tier.SAFE
        assert permission_engine.get_tier("file_read") == Tier.NOTIFY
        assert permission_engine.get_tier("web_browse") == Tier.NOTIFY
        assert permission_engine.get_tier("screen_capture") == Tier.ASK
        assert permission_engine.get_tier("file_write") == Tier.ASK
        assert permission_engine.get_tier("access_credentials") == Tier.BLOCK
        assert permission_engine.get_tier("fork_bomb") == Tier.BLOCK


# ── Feature 10: Audit Preview Mode ────────────────────────────────────────────

class TestAuditPreviewMode:
    """ActionPreview is populated with correct structure before ASK actions."""

    async def test_approval_callback_receives_action_preview(self, permission_engine):
        """The approval callback receives an ActionPreview with all required fields."""
        received = []

        async def capture_preview(preview):
            received.append(preview)
            return True

        permission_engine.set_approval_callback(capture_preview)

        await permission_engine.check(
            action_type="file_write",
            description="Write to ~/Documents/output.txt",
            details={"path": "~/Documents/output.txt", "size": 512},
            source="skill:file_manager",
            session_id="s1",
            reversible=False,
            risk_level="medium",
        )

        assert len(received) == 1
        d = received[0].to_dict()
        assert d["action_type"] == "file_write"
        assert d["description"] == "Write to ~/Documents/output.txt"
        assert d["reversible"] is False
        assert d["risk_level"] == "medium"
        assert "tier" in d

    async def test_format_for_terminal_returns_human_readable_text(self):
        """format_for_terminal() returns a readable string containing the description."""
        from duckclaw.permissions.engine import ActionPreview, Tier

        preview = ActionPreview(
            action_type="screen_capture",
            tier=Tier.ASK,
            description="Take a screenshot of your screen",
            details={"monitor": 0},
            reversible=True,
            risk_level="low",
        )

        text = preview.format_for_terminal()
        assert isinstance(text, str)
        assert len(text) > 20
        assert "screenshot" in text.lower() or "screen" in text.lower()

    async def test_format_for_terminal_includes_risk_and_reversibility(self):
        """format_for_terminal() surfaces risk level and reversibility."""
        from duckclaw.permissions.engine import ActionPreview, Tier

        preview = ActionPreview(
            action_type="file_delete",
            tier=Tier.ASK,
            description="Delete important file",
            details={},
            reversible=False,
            risk_level="high",
        )

        text = preview.format_for_terminal()
        assert "HIGH" in text or "high" in text.lower()
        assert "Irreversible" in text or "irreversible" in text.lower()

    async def test_safe_action_never_triggers_approval_callback(self, permission_engine):
        """SAFE actions do not invoke the approval callback — no preview needed."""
        approval_called = []

        async def should_not_be_called(preview):
            approval_called.append(preview)
            return True

        permission_engine.set_approval_callback(should_not_be_called)

        await permission_engine.check(
            action_type="web_search",
            description="Search DuckDuckGo",
            source="test",
            session_id="s1",
        )

        assert len(approval_called) == 0


# ── Feature 11: Full Audit Log ────────────────────────────────────────────────

class TestAuditLog:
    """Every action is logged; entries are filterable and exportable."""

    async def test_every_permission_check_writes_one_audit_row(self, permission_engine):
        """Each check() call produces exactly one audit log entry."""
        await permission_engine.check(
            action_type="memory_read", description="Read memory", source="test", session_id="s1"
        )
        await permission_engine.check(
            action_type="web_search", description="Search web", source="test", session_id="s1"
        )

        logs = permission_engine.get_audit_log(limit=10)
        assert len(logs) >= 2

    async def test_audit_log_filter_by_status_blocked(self, permission_engine):
        """get_audit_log(status='blocked') returns only blocked entries."""
        await permission_engine.check(
            action_type="access_credentials",
            description="Try to access credentials",
            source="test",
            session_id="s1",
        )
        await permission_engine.check(
            action_type="memory_read", description="Safe read", source="test", session_id="s1"
        )

        blocked = permission_engine.get_audit_log(status="blocked")
        assert len(blocked) >= 1
        assert all(e["status"] == "blocked" for e in blocked)

    async def test_audit_log_filter_by_action_type(self, permission_engine):
        """get_audit_log(action_type=...) returns only matching entries."""
        await permission_engine.check(
            action_type="memory_read", description="Read facts", source="test", session_id="s1"
        )
        await permission_engine.check(
            action_type="web_search", description="Search", source="test", session_id="s1"
        )

        logs = permission_engine.get_audit_log(action_type="memory_read")
        assert all(e["action_type"] == "memory_read" for e in logs)
        assert len(logs) >= 1

    async def test_audit_log_entry_has_required_fields(self, permission_engine):
        """Each audit entry contains the required schema fields."""
        await permission_engine.check(
            action_type="file_read",
            description="Read test.txt",
            source="skill:file_manager",
            session_id="session-abc",
            reversible=True,
            risk_level="low",
        )

        logs = permission_engine.get_audit_log(limit=1)
        assert len(logs) >= 1
        entry = logs[0]
        for field in ("id", "timestamp", "action_type", "tier", "description", "status", "source"):
            assert field in entry, f"Missing field: {field}"

    async def test_export_audit_log_json_is_parseable(self, permission_engine):
        """export_audit_log(fmt='json') returns valid JSON list."""
        await permission_engine.check(
            action_type="memory_read", description="Read memory", source="test", session_id="s1"
        )

        exported = permission_engine.export_audit_log(fmt="json")
        parsed = json.loads(exported)
        assert isinstance(parsed, list)
        assert len(parsed) >= 1

    async def test_export_audit_log_csv_has_header_and_data(self, permission_engine):
        """export_audit_log(fmt='csv') produces CSV with header row and data."""
        await permission_engine.check(
            action_type="memory_read", description="Read memory", source="test", session_id="s1"
        )

        exported = permission_engine.export_audit_log(fmt="csv")
        lines = exported.strip().splitlines()
        assert len(lines) >= 2
        header = lines[0].lower()
        assert "action_type" in header or "timestamp" in header

    async def test_audit_stats_reflect_actual_counts(self, permission_engine):
        """get_audit_stats() reflects the actual actions taken."""
        permission_engine.set_approval_callback(AsyncMock(return_value=True))

        await permission_engine.check(
            action_type="memory_read", description="Safe", source="test", session_id="s1"
        )
        await permission_engine.check(
            action_type="access_credentials", description="Blocked", source="test", session_id="s1"
        )

        stats = permission_engine.get_audit_stats()
        assert isinstance(stats, dict)
        assert stats["total_actions"] >= 2
        assert stats["blocked"] >= 1
