
import json

import pytest

from cyberbrain26.agents.orchestrator import (
    BROWSER_CAPABILITY_UNAVAILABLE_REASON,
    BROWSER_TASK_COMPLETED,
    BROWSER_TASK_REQUEST,
    CODER_TASK_COMPLETED,
    CODER_TASK_REQUEST,
    EVALUATOR_TASK_REQUEST,
    MAX_PLAN_STEPS,
    PLANNER_TASK_COMPLETED,
    PLANNER_TASK_REQUEST,
    RESEARCH_TASK_REQUEST,
    SECURITY_TASK_REQUEST,
    AgentResult,
    BaseAgent,
    BrowserAgent,
    CoderAgent,
    DispatcherAuthorizer,
    EvaluatorAgent,
    PlannerAgent,
    ResearchAgent,
    SecurityAgent,
)
from cyberbrain26.control_plane import AsyncEventBus, Event, MemoryTaskQueue
from cyberbrain26.security_plane.permission import SecurityError


class AllowingAuthorizer:
    """Test double for the TaskAuthorizer contract: returns without raising."""

    def __init__(self) -> None:
        self.calls: list[dict] = []

    async def __call__(self, task_data: dict) -> None:
        self.calls.append(dict(task_data))


class DenyingAuthorizer:
    """Test double for the TaskAuthorizer contract: raises like the dispatcher."""

    def __init__(self, reason: str = "denied by policy") -> None:
        self.reason = reason
        self.calls: list[dict] = []

    async def __call__(self, task_data: dict) -> None:
        self.calls.append(dict(task_data))
        raise SecurityError(self.reason)


class BrokenAuthorizer:
    """Authorizer whose decision cannot be made at all."""

    async def __call__(self, task_data: dict) -> None:
        raise RuntimeError("permission gateway unavailable")


class RecordingDispatcher:
    """Minimal structural stand-in for InMemoryTaskDispatcher._authorize."""

    def __init__(self, deny: bool = False) -> None:
        self.deny = deny
        self.calls: list[tuple] = []

    async def _authorize(
        self, task_id: str, registered_agent_id: str, agent: object, action: str, resource=None
    ) -> None:
        self.calls.append((task_id, registered_agent_id, agent, action, resource))
        if self.deny:
            raise SecurityError(f"Agent {registered_agent_id} denied for action '{action}'")


@pytest.fixture
def bus():
    return AsyncEventBus()


@pytest.fixture
def queue():
    return MemoryTaskQueue()


class TestPlannerAgent:
    def test_planner_agent_name(self, bus, queue):
        agent = PlannerAgent(bus, queue)
        assert agent.name == "PlannerAgent"
        assert agent.role == "planner"

    def test_planner_subscribes_to_events(self, bus, queue):
        PlannerAgent(bus, queue)
        assert bus.subscribers(PLANNER_TASK_REQUEST) == 1
        assert bus.subscribers(PLANNER_TASK_COMPLETED) == 0

    @pytest.mark.asyncio
    async def test_planner_execute_returns_result(self, bus, queue):
        agent = PlannerAgent(bus, queue)
        result = await agent.execute({"goal": "test goal"})
        assert result is not None
        assert result.success is True
        assert result.agent_name == "PlannerAgent"
        assert result.data["goal"] == "test goal"
        assert len(result.data["plan"]) == 3
        assert result.data["max_steps"] == 3

    @pytest.mark.asyncio
    async def test_planner_execute_tracks_results(self, bus, queue):
        agent = PlannerAgent(bus, queue)
        await agent.execute({"goal": "test goal"})
        assert len(agent.results) == 1
        assert isinstance(agent.results[0], AgentResult)

    @pytest.mark.asyncio
    async def test_planner_get_plan(self, bus, queue):
        agent = PlannerAgent(bus, queue)
        await agent.execute({"goal": "build feature"})
        plan = agent.get_plan()
        assert len(plan) == 3
        assert plan[0]["description"] == "build feature - step 1"


class TestPlannerMaxSteps:
    """``max_steps`` used to be ignored: the plan was hard-coded to 3 steps."""

    @pytest.mark.asyncio
    async def test_planner_honours_max_steps(self, bus, queue):
        agent = PlannerAgent(bus, queue)
        result = await agent.execute({"goal": "ship it", "max_steps": 7})
        assert result.success is True
        assert len(result.data["plan"]) == 7
        assert result.data["max_steps"] == 7
        assert [step["step"] for step in result.data["plan"]] == list(range(1, 8))
        assert agent.get_plan()[-1]["description"] == "ship it - step 7"

    @pytest.mark.asyncio
    async def test_planner_honours_single_step(self, bus, queue):
        agent = PlannerAgent(bus, queue)
        result = await agent.execute({"goal": "one", "max_steps": 1})
        assert len(result.data["plan"]) == 1

    @pytest.mark.asyncio
    async def test_planner_absent_max_steps_keeps_default(self, bus, queue):
        agent = PlannerAgent(bus, queue)
        result = await agent.execute({"goal": "default"})
        assert len(result.data["plan"]) == 3

    @pytest.mark.asyncio
    async def test_planner_none_max_steps_keeps_default(self, bus, queue):
        agent = PlannerAgent(bus, queue)
        result = await agent.execute({"goal": "default", "max_steps": None})
        assert result.success is True
        assert len(result.data["plan"]) == 3

    @pytest.mark.asyncio
    @pytest.mark.parametrize("bad", [0, -1, -100])
    async def test_planner_rejects_non_positive_max_steps(self, bus, queue, bad):
        agent = PlannerAgent(bus, queue)
        result = await agent.execute({"goal": "bad", "max_steps": bad})
        assert result.success is False
        assert "greater than zero" in result.error
        assert result.execution_attempted is False
        assert agent.get_plan() == []

    @pytest.mark.asyncio
    @pytest.mark.parametrize("bad", ["3", 3.5, True, False, [3], {"steps": 3}])
    async def test_planner_rejects_non_integer_max_steps(self, bus, queue, bad):
        agent = PlannerAgent(bus, queue)
        result = await agent.execute({"goal": "bad", "max_steps": bad})
        assert result.success is False
        assert "positive integer" in result.error
        assert result.execution_attempted is False
        assert agent.get_plan() == []

    @pytest.mark.asyncio
    async def test_planner_caps_very_large_max_steps(self, bus, queue):
        agent = PlannerAgent(bus, queue)
        result = await agent.execute({"goal": "huge", "max_steps": 10**9})
        assert result.success is True
        assert result.data["max_steps"] == MAX_PLAN_STEPS
        assert len(result.data["plan"]) == MAX_PLAN_STEPS

    @pytest.mark.asyncio
    async def test_planner_exact_cap_is_not_reduced(self, bus, queue):
        agent = PlannerAgent(bus, queue)
        result = await agent.execute({"goal": "cap", "max_steps": MAX_PLAN_STEPS})
        assert result.data["max_steps"] == MAX_PLAN_STEPS
        assert len(result.data["plan"]) == MAX_PLAN_STEPS


class TestCoderAgent:
    def test_coder_agent_name(self, bus, queue):
        agent = CoderAgent(bus, queue)
        assert agent.name == "CoderAgent"
        assert agent.role == "coder"

    def test_coder_subscribes_to_events(self, bus, queue):
        CoderAgent(bus, queue)
        assert bus.subscribers(CODER_TASK_REQUEST) == 1

    @pytest.mark.asyncio
    async def test_coder_execute_returns_result(self, bus, queue):
        agent = CoderAgent(bus, queue)
        result = await agent.execute({"description": "implement API"})
        assert result is not None
        assert result.success is True
        assert result.data["description"] == "implement API"
        assert result.data["status"] == "implemented"

    @pytest.mark.asyncio
    async def test_coder_get_implemented(self, bus, queue):
        agent = CoderAgent(bus, queue)
        await agent.execute({"description": "implement API"})
        implemented = agent.get_implemented()
        assert len(implemented) == 1
        assert implemented[0] == "implement API"


class TestEvaluatorAgent:
    def test_evaluator_agent_name(self, bus, queue):
        agent = EvaluatorAgent(bus, queue)
        assert agent.name == "EvaluatorAgent"
        assert agent.role == "evaluator"

    def test_evaluator_subscribes_to_events(self, bus, queue):
        EvaluatorAgent(bus, queue)
        assert bus.subscribers(EVALUATOR_TASK_REQUEST) == 1

    @pytest.mark.asyncio
    async def test_evaluator_execute_passes(self, bus, queue):
        agent = EvaluatorAgent(bus, queue)
        result = await agent.execute({"score": 0.8})
        assert result is not None
        assert result.success is True
        assert result.data["passed"] is True
        assert result.data["score"] == 0.8

    @pytest.mark.asyncio
    async def test_evaluator_execute_fails(self, bus, queue):
        agent = EvaluatorAgent(bus, queue)
        result = await agent.execute({"score": 0.3})
        assert result is not None
        assert result.success is False
        assert result.data["passed"] is False

    @pytest.mark.asyncio
    async def test_evaluator_get_assessments(self, bus, queue):
        agent = EvaluatorAgent(bus, queue)
        await agent.execute({"score": 0.9})
        assessments = agent.get_assessments()
        assert len(assessments) == 1
        assert assessments[0]["passed"] is True


class TestSecurityAgent:
    def test_security_agent_name(self, bus, queue):
        agent = SecurityAgent(bus, queue)
        assert agent.name == "SecurityAgent"
        assert agent.role == "security"

    def test_security_subscribes_to_events(self, bus, queue):
        SecurityAgent(bus, queue)
        assert bus.subscribers(SECURITY_TASK_REQUEST) == 1

    @pytest.mark.asyncio
    async def test_security_execute_approved(self, bus, queue):
        agent = SecurityAgent(bus, queue)
        result = await agent.execute({"requires_review": False})
        assert result is not None
        assert result.success is True
        assert result.data["requires_review"] is False
        assert result.data["severity"] == "low"

    @pytest.mark.asyncio
    async def test_security_execute_blocked(self, bus, queue):
        agent = SecurityAgent(bus, queue)
        result = await agent.execute({"requires_review": True})
        assert result is not None
        assert result.success is False
        assert result.data["requires_review"] is True
        assert result.data["severity"] == "high"

    @pytest.mark.asyncio
    async def test_security_get_findings(self, bus, queue):
        agent = SecurityAgent(bus, queue)
        await agent.execute({"requires_review": True})
        findings = agent.get_findings()
        assert len(findings) == 1
        assert findings[0]["severity"] == "high"


class TestResearchAgent:
    def test_research_agent_name(self, bus, queue):
        agent = ResearchAgent(bus, queue)
        assert agent.name == "ResearchAgent"
        assert agent.role == "research"

    def test_research_subscribes_to_events(self, bus, queue):
        ResearchAgent(bus, queue)
        assert bus.subscribers(RESEARCH_TASK_REQUEST) == 1

    @pytest.mark.asyncio
    async def test_research_execute_returns_result(self, bus, queue):
        agent = ResearchAgent(bus, queue)
        result = await agent.execute({"topic": "AI safety"})
        assert result is not None
        assert result.success is True
        assert result.data["topic"] == "AI safety"

    @pytest.mark.asyncio
    async def test_research_get_research(self, bus, queue):
        agent = ResearchAgent(bus, queue)
        await agent.execute({"topic": "AI safety"})
        research = agent.get_research()
        assert len(research) == 1
        assert research[0]["topic"] == "AI safety"


class TestAgentEventBusAuthorization:
    """The bus must not be a way around the authorization chain.

    A published ``*_TASK_REQUEST`` is untrusted input: anyone holding a
    reference to the bus could previously reach ``Agent.execute`` with an
    arbitrary payload and no identity, scope, policy or approval check.
    """

    @pytest.mark.asyncio
    async def test_publish_without_authorizer_is_refused(self, bus, queue):
        planner = PlannerAgent(bus, queue)
        await bus.publish(PLANNER_TASK_REQUEST, {"goal": "integration test"})
        assert len(planner.results) == 1
        assert planner.results[0].success is False
        assert "authorizer" in planner.results[0].error
        assert planner.results[0].execution_attempted is False
        assert planner.results[0].data["source"] == "event_bus"
        # The agent body must not have run at all.
        assert planner.get_plan() == []

    @pytest.mark.asyncio
    async def test_refusal_keeps_publisher_task_id(self, bus, queue):
        planner = PlannerAgent(bus, queue)
        await bus.publish(PLANNER_TASK_REQUEST, {"goal": "g", "task_id": "t-42"})
        assert planner.results[0].task_id == "t-42"

    @pytest.mark.asyncio
    @pytest.mark.parametrize(
        "event, factory",
        [
            (PLANNER_TASK_REQUEST, lambda b, q: PlannerAgent(b, q)),
            (CODER_TASK_REQUEST, lambda b, q: CoderAgent(b, q)),
            (EVALUATOR_TASK_REQUEST, lambda b, q: EvaluatorAgent(b, q)),
            (SECURITY_TASK_REQUEST, lambda b, q: SecurityAgent(b, q)),
            (RESEARCH_TASK_REQUEST, lambda b, q: ResearchAgent(b, q)),
            (BROWSER_TASK_REQUEST, lambda b, q: BrowserAgent(b, q)),
        ],
    )
    async def test_every_agent_refuses_without_authorizer(self, bus, queue, event, factory):
        agent = factory(bus, queue)
        await bus.publish(event, {"description": "d", "score": 0.9})
        assert len(agent.results) == 1
        assert agent.results[0].success is False
        assert agent.results[0].error is not None

    @pytest.mark.asyncio
    async def test_denying_authorizer_prevents_agent_body(self, bus, queue):
        authorizer = DenyingAuthorizer("scope gate")
        planner = PlannerAgent(bus, queue, authorizer=authorizer)
        coder = CoderAgent(bus, queue, authorizer=authorizer)
        await bus.publish(PLANNER_TASK_REQUEST, {"goal": "denied"})
        await bus.publish(CODER_TASK_REQUEST, {"description": "denied"})
        assert planner.results[0].success is False
        assert planner.results[0].error == "scope gate"
        assert planner.get_plan() == []
        assert coder.results[0].success is False
        assert coder.get_implemented() == []

    @pytest.mark.asyncio
    async def test_approving_authorizer_allows_execution(self, bus, queue):
        authorizer = AllowingAuthorizer()
        planner = PlannerAgent(bus, queue, authorizer=authorizer)
        await bus.publish(PLANNER_TASK_REQUEST, {"goal": "integration test"})
        assert len(planner.results) == 1
        assert planner.results[0].success is True
        assert planner.results[0].data["goal"] == "integration test"
        assert authorizer.calls == [{"goal": "integration test"}]

    @pytest.mark.asyncio
    async def test_authorizer_must_be_consulted_before_execution(self, bus, queue):
        """Ordering matters: the authorizer runs, and only then the body."""
        order: list[str] = []

        class RecordingAuthorizer:
            async def __call__(self, task_data: dict) -> None:
                order.append("authorize")

        security = SecurityAgent(bus, queue, authorizer=RecordingAuthorizer())
        original_execute = security.execute

        async def traced_execute(task_data: dict):
            order.append("execute")
            return await original_execute(task_data)

        security.execute = traced_execute  # type: ignore[method-assign]
        await bus.publish(SECURITY_TASK_REQUEST, {"requires_review": False})
        assert order == ["authorize", "execute"]

    @pytest.mark.asyncio
    async def test_broken_authorizer_fails_closed(self, bus, queue):
        planner = PlannerAgent(bus, queue, authorizer=BrokenAuthorizer())
        await bus.publish(PLANNER_TASK_REQUEST, {"goal": "g"})
        assert planner.results[0].success is False
        assert "failed closed" in planner.results[0].error
        assert planner.get_plan() == []

    @pytest.mark.asyncio
    async def test_authorizer_can_be_assigned_after_construction(self, bus, queue):
        """The contract the bridge must satisfy in the next wave."""
        planner = PlannerAgent(bus, queue)
        dispatcher = RecordingDispatcher()
        planner.authorizer = DispatcherAuthorizer(
            dispatcher, "ptah-planner", planner
        )
        await bus.publish(PLANNER_TASK_REQUEST, {"goal": "g"})
        assert planner.results[0].success is True
        task_id, agent_id, agent, action, resource = dispatcher.calls[0]
        assert agent_id == "ptah-planner"
        assert agent is planner
        assert action == "execute"
        assert resource is None
        assert task_id.startswith("bus-")

    @pytest.mark.asyncio
    async def test_dispatcher_authorizer_refusal_blocks_execution(self, bus, queue):
        planner = PlannerAgent(bus, queue)
        planner.authorizer = DispatcherAuthorizer(
            RecordingDispatcher(deny=True), "ptah-planner", planner
        )
        await bus.publish(PLANNER_TASK_REQUEST, {"goal": "g"})
        assert planner.results[0].success is False
        assert "denied" in planner.results[0].error
        assert planner.get_plan() == []


class TestAgentEventBusIntegration:
    @pytest.mark.asyncio
    async def test_publish_event_triggers_authorized_agent(self, bus, queue):
        planner = PlannerAgent(bus, queue, authorizer=AllowingAuthorizer())
        await bus.publish(PLANNER_TASK_REQUEST, {"goal": "integration test"})
        assert len(planner.results) == 1
        assert planner.results[0].success is True
        assert planner.results[0].data["goal"] == "integration test"

    @pytest.mark.asyncio
    async def test_agent_publish_completed_event(self, bus, queue):
        CoderAgent(bus, queue, authorizer=AllowingAuthorizer())
        completed_events: list[dict] = []

        async def on_completed(event: Event) -> None:
            completed_events.append(event.payload)

        bus.subscribe(CODER_TASK_COMPLETED, on_completed)
        await bus.publish(CODER_TASK_REQUEST, {"description": "test"})
        assert len(completed_events) == 1
        assert completed_events[0]["agent"] == "CoderAgent"
        assert completed_events[0]["result"]["success"] is True

    @pytest.mark.asyncio
    async def test_refusal_is_published_as_completed_event(self, bus, queue):
        PlannerAgent(bus, queue)
        completed_events: list[dict] = []

        async def on_completed(event: Event) -> None:
            completed_events.append(event.payload)

        bus.subscribe(PLANNER_TASK_COMPLETED, on_completed)
        await bus.publish(PLANNER_TASK_REQUEST, {"goal": "g"})
        assert len(completed_events) == 1
        assert completed_events[0]["agent"] == "PlannerAgent"
        assert completed_events[0]["result"]["success"] is False

    @pytest.mark.asyncio
    async def test_multiple_agents_on_same_bus(self, bus, queue):
        authorizer = AllowingAuthorizer()
        planner = PlannerAgent(bus, queue, authorizer=authorizer)
        coder = CoderAgent(bus, queue, authorizer=authorizer)
        security = SecurityAgent(bus, queue, authorizer=authorizer)
        await bus.publish(PLANNER_TASK_REQUEST, {"goal": "multi"})
        await bus.publish(CODER_TASK_REQUEST, {"description": "code"})
        await bus.publish(SECURITY_TASK_REQUEST, {"requires_review": False})
        assert len(planner.results) == 1
        assert len(coder.results) == 1
        assert len(security.results) == 1


class TestBrowserAgent:
    """BrowserAgent has no backend: it must not report a browse it never did."""

    def test_browser_agent_name(self, bus, queue):
        agent = BrowserAgent(bus, queue)
        assert agent.name == "BrowserAgent"
        assert agent.role == "browser"

    def test_browser_subscribes_to_events(self, bus, queue):
        BrowserAgent(bus, queue)
        assert bus.subscribers(BROWSER_TASK_REQUEST) == 1

    @pytest.mark.asyncio
    async def test_browser_does_not_claim_success(self, bus, queue):
        agent = BrowserAgent(bus, queue)
        result = await agent.execute({"url": "https://example.invalid"})
        assert result is not None
        assert result.success is False
        assert result.error == BROWSER_CAPABILITY_UNAVAILABLE_REASON
        assert result.data["status"] == "capability_unavailable"
        assert result.data["capability"] == "browsing"
        assert result.data["url"] == "https://example.invalid"
        assert "Browsed" not in json.dumps(result.data)

    @pytest.mark.asyncio
    async def test_browser_records_nothing(self, bus, queue):
        agent = BrowserAgent(bus, queue, authorizer=AllowingAuthorizer())
        await agent.execute({"url": "https://example.invalid"})
        await bus.publish(BROWSER_TASK_REQUEST, {"url": "https://example.invalid"})
        assert agent.get_browsed() == []
        assert all(result.success is False for result in agent.results)

    @pytest.mark.asyncio
    async def test_browser_publishes_failure_completed_event(self, bus, queue):
        BrowserAgent(bus, queue, authorizer=AllowingAuthorizer())
        completed_events: list[dict] = []

        async def on_completed(event: Event) -> None:
            completed_events.append(event.payload)

        bus.subscribe(BROWSER_TASK_COMPLETED, on_completed)
        await bus.publish(BROWSER_TASK_REQUEST, {"url": "https://example.invalid"})
        assert len(completed_events) == 1
        assert completed_events[0]["result"]["success"] is False
        assert (
            completed_events[0]["result"]["error"] == BROWSER_CAPABILITY_UNAVAILABLE_REASON
        )


class TestAgentTaskQueueIntegration:
    @pytest.mark.asyncio
    async def test_agent_enqueues_tasks(self, bus, queue):
        agent = PlannerAgent(bus, queue)
        await agent.execute({"goal": "queue test"})
        assert queue.size >= 0
        assert len(agent.results) == 1

    @pytest.mark.asyncio
    async def test_agent_task_records_in_queue(self, bus, queue):
        initial_size = queue.size
        planner = PlannerAgent(bus, queue)
        await planner.execute({"goal": "record test"})
        assert queue.size >= initial_size

    @pytest.mark.asyncio
    async def test_evaluator_with_queue_run(self, bus, queue):
        evaluator = EvaluatorAgent(bus, queue)
        result = await evaluator.execute({"score": 0.7})
        assert result is not None
        assert result.success is True

    @pytest.mark.asyncio
    async def test_security_with_queue_run(self, bus, queue):
        agent = SecurityAgent(bus, queue)
        result = await agent.execute({"requires_review": False})
        assert result is not None
        assert result.success is True


class TestBaseAgentAbstract:
    def test_base_agent_is_abstract(self):
        with pytest.raises(TypeError):
            BaseAgent(AsyncEventBus(), MemoryTaskQueue())
