from __future__ import annotations

from pydantic import BaseModel
import pytest

from tests.conftest import build_test_agent_loop, build_test_vibe_config
from tests.mock.utils import mock_llm_chunk
from tests.stubs.fake_backend import FakeBackend
from vibe.core.tools.base import ToolPermission
from vibe.core.tools.permissions import (
    ApprovedRule,
    PermissionScope,
    PermissionStore,
    RequiredPermission,
)
from vibe.core.types import ApprovalResponse, FunctionCall, ToolCall, ToolResultEvent


class TestPermissionStore:
    def test_covers_returns_false_when_empty(self):
        store = PermissionStore()
        rp = RequiredPermission(
            scope=PermissionScope.COMMAND_PATTERN,
            invocation_pattern="npm install foo",
            session_pattern="npm install *",
            label="npm install *",
        )

        assert not store.covers("bash", rp)

    def test_covers_returns_true_after_matching_rule_added(self):
        store = PermissionStore()
        store.add_rule(
            ApprovedRule(
                tool_name="bash",
                scope=PermissionScope.COMMAND_PATTERN,
                session_pattern="npm install *",
            )
        )
        rp = RequiredPermission(
            scope=PermissionScope.COMMAND_PATTERN,
            invocation_pattern="npm install foo",
            session_pattern="npm install *",
            label="npm install *",
        )

        assert store.covers("bash", rp)

    def test_covers_isolates_by_tool_name(self):
        store = PermissionStore()
        store.add_rule(
            ApprovedRule(
                tool_name="bash",
                scope=PermissionScope.COMMAND_PATTERN,
                session_pattern="npm install *",
            )
        )
        rp = RequiredPermission(
            scope=PermissionScope.COMMAND_PATTERN,
            invocation_pattern="npm install foo",
            session_pattern="npm install *",
            label="npm install *",
        )

        assert not store.covers("read", rp)

    def test_tool_permission_round_trip(self):
        store = PermissionStore()
        assert store.get_tool_permission("bash") is None

        store.set_tool_permission("bash", ToolPermission.ALWAYS)

        assert store.get_tool_permission("bash") == ToolPermission.ALWAYS


class TestAgentLoopSharesStore:
    def test_subagent_inherits_parent_session_rules(self):
        store = PermissionStore()
        parent = build_test_agent_loop(permission_store=store)
        subagent = build_test_agent_loop(permission_store=store)

        parent.approve_always(
            "bash",
            [
                RequiredPermission(
                    scope=PermissionScope.COMMAND_PATTERN,
                    invocation_pattern="npm install foo",
                    session_pattern="npm install *",
                    label="npm install *",
                )
            ],
        )

        rp = RequiredPermission(
            scope=PermissionScope.COMMAND_PATTERN,
            invocation_pattern="npm install bar",
            session_pattern="npm install *",
            label="npm install *",
        )
        assert subagent._permission_store.covers("bash", rp)

    def test_subagent_inherits_parent_tool_permission(self):
        store = PermissionStore()
        parent = build_test_agent_loop(permission_store=store)
        subagent = build_test_agent_loop(permission_store=store)

        parent.approve_always("bash", None)

        assert (
            subagent._permission_store.get_tool_permission("bash")
            == ToolPermission.ALWAYS
        )

    @pytest.mark.asyncio
    async def test_subagent_applies_parent_tool_permission_before_resolution(self):
        store = PermissionStore()
        parent = build_test_agent_loop(permission_store=store)
        parent.approve_always("bash", None)
        approval_requested = False

        async def approval_callback(
            tool_name: str,
            args: BaseModel,
            tool_call_id: str,
            required_permissions: list[RequiredPermission] | None,
        ) -> tuple[ApprovalResponse, str | None]:
            nonlocal approval_requested
            approval_requested = True
            return ApprovalResponse.NO, None

        tool_call = ToolCall(
            id="call_1",
            index=0,
            function=FunctionCall(name="bash", arguments='{"command":"true"}'),
        )
        subagent = build_test_agent_loop(
            config=build_test_vibe_config(enabled_tools=["bash"]),
            backend=FakeBackend([
                [mock_llm_chunk(content="Running it.", tool_calls=[tool_call])],
                [mock_llm_chunk(content="Done.")],
            ]),
            permission_store=store,
        )
        subagent.set_approval_callback(approval_callback)

        events = [event async for event in subagent.act("run true")]
        tool_results = [event for event in events if isinstance(event, ToolResultEvent)]

        assert not approval_requested
        assert len(tool_results) == 1
        assert tool_results[0].skipped is False
        assert "permission" not in subagent.config.tools.get("bash", {})

    def test_default_store_is_per_loop_when_not_shared(self):
        a = build_test_agent_loop()
        b = build_test_agent_loop()

        assert a._permission_store is not b._permission_store
        assert a._permission_store.lock is not b._permission_store.lock

    def test_subagent_shares_parent_approval_lock(self):
        store = PermissionStore()
        parent = build_test_agent_loop(permission_store=store)
        subagent = build_test_agent_loop(permission_store=store)

        assert parent._permission_store.lock is subagent._permission_store.lock
