"""Tests for ToolExecutor — async job queue and dispatch."""

import asyncio
from unittest.mock import patch

import pytest

from proxym.dashboard.tickets import Ticket, TicketType, TicketPriority
from proxym.tools import ToolDefinition, ToolCategory, ToolCapability
from proxym.tools.adapters import ToolResult, BaseAdapter, register_adapter, _ADAPTER_MAP
from proxym.tools.executor import ToolExecutor, ToolJob, JobStatus


# ── Fake adapter for testing ─────────────────────────────

class FakeAdapter(BaseAdapter):
    """Adapter that returns instantly without running a real process."""

    def build_args(self, prompt, project_dir, mode="", model="", files=None):
        return ["echo", "fake"]

    async def run(self, prompt, project_dir, ticket_id="", mode="", model="",
                  files=None, timeout=300):
        return ToolResult(
            tool=self.tool.name,
            ticket_id=ticket_id,
            exit_code=0,
            stdout="fake output",
            duration_s=0.01,
        )


class FakeFailAdapter(BaseAdapter):
    """Adapter that always fails."""

    def build_args(self, prompt, project_dir, mode="", model="", files=None):
        return ["false"]

    async def run(self, prompt, project_dir, ticket_id="", mode="", model="",
                  files=None, timeout=300):
        return ToolResult(
            tool=self.tool.name,
            ticket_id=ticket_id,
            exit_code=1,
            stderr="simulated failure",
            duration_s=0.01,
        )


@pytest.fixture
def fake_tool():
    from proxym.tools import ToolRegistry
    ToolRegistry._instance = None
    reg = ToolRegistry.get()
    tool = ToolDefinition(
        name="fake-tool",
        category=ToolCategory.AGENT_CLI,
        binary="echo",
        capabilities=[ToolCapability.CODE_EDIT],
        modes=["code"],
        default_model="test-model",
    )
    reg.register(tool)
    register_adapter("fake-tool", FakeAdapter)
    yield tool
    reg.unregister("fake-tool")
    _ADAPTER_MAP.pop("fake-tool", None)
    ToolRegistry._instance = None


@pytest.fixture
def fail_tool():
    from proxym.tools import ToolRegistry
    ToolRegistry._instance = None
    reg = ToolRegistry.get()
    tool = ToolDefinition(
        name="fail-tool",
        category=ToolCategory.AGENT_CLI,
        binary="false",
        capabilities=[ToolCapability.CODE_EDIT],
        modes=["code"],
    )
    reg.register(tool)
    register_adapter("fail-tool", FakeFailAdapter)
    yield tool
    reg.unregister("fail-tool")
    _ADAPTER_MAP.pop("fail-tool", None)
    ToolRegistry._instance = None


@pytest.fixture
def sample_ticket():
    return Ticket(
        id="TKT-EXEC01",
        title="Test executor dispatch",
        type=TicketType.TASK,
        priority=TicketPriority.MEDIUM,
    )


@pytest.fixture
def executor():
    ToolExecutor._instance = None
    ex = ToolExecutor(max_concurrent=1)
    ToolExecutor._instance = ex
    yield ex
    ToolExecutor._instance = None


class TestToolJob:
    def test_summary_keys(self):
        j = ToolJob(ticket_id="TKT-1", tool_name="aider")
        s = j.summary()
        assert "id" in s
        assert "status" in s
        assert s["ticket_id"] == "TKT-1"
        assert s["tool"] == "aider"
        assert s["status"] == "queued"


class TestToolExecutor:
    def test_enqueue(self, executor, fake_tool, sample_ticket):
        job = executor.enqueue(sample_ticket, fake_tool, "/tmp/test-project")
        assert job.status == JobStatus.QUEUED
        assert job.tool_name == "fake-tool"
        assert job.ticket_id == "TKT-EXEC01"
        assert "Test executor dispatch" in job.prompt

    def test_get_job(self, executor, fake_tool, sample_ticket):
        job = executor.enqueue(sample_ticket, fake_tool, "/tmp")
        found = executor.get_job(job.id)
        assert found is job

    def test_get_job_missing(self, executor):
        assert executor.get_job("nonexistent") is None

    def test_list_jobs(self, executor, fake_tool, sample_ticket):
        executor.enqueue(sample_ticket, fake_tool, "/tmp")
        jobs = executor.list_jobs()
        assert len(jobs) == 1

    def test_cancel_queued_job(self, executor, fake_tool, sample_ticket):
        job = executor.enqueue(sample_ticket, fake_tool, "/tmp")
        assert executor.cancel_job(job.id) is True
        assert job.status == JobStatus.CANCELLED

    def test_cancel_missing_job(self, executor):
        assert executor.cancel_job("nonexistent") is False

    def test_queue_summary(self, executor, fake_tool, sample_ticket):
        executor.enqueue(sample_ticket, fake_tool, "/tmp")
        s = executor.queue_summary()
        assert s["total_jobs"] == 1
        assert s["queue_size"] == 1
        assert "queued" in s["by_status"]

    @pytest.mark.asyncio
    async def test_execute_job_success(self, executor, fake_tool, sample_ticket):
        job = executor.enqueue(sample_ticket, fake_tool, "/tmp")
        await executor._execute_job(job)
        assert job.status == JobStatus.DONE
        assert job.result is not None
        assert job.result.success is True
        assert job.result.stdout == "fake output"

    @pytest.mark.asyncio
    async def test_execute_job_failure(self, executor, fail_tool, sample_ticket):
        job = executor.enqueue(sample_ticket, fail_tool, "/tmp")
        await executor._execute_job(job)
        assert job.status == JobStatus.FAILED
        assert "simulated failure" in job.error

    @pytest.mark.asyncio
    async def test_execute_job_no_adapter(self, executor, sample_ticket):
        tool = ToolDefinition(name="no-adapter", category=ToolCategory.CUSTOM)
        from proxym.tools import ToolRegistry
        ToolRegistry.get().register(tool)
        job = executor.enqueue(sample_ticket, tool, "/tmp")
        await executor._execute_job(job)
        assert job.status == JobStatus.FAILED
        assert "No adapter" in job.error
        ToolRegistry.get().unregister("no-adapter")

    @pytest.mark.asyncio
    async def test_start_stop(self, executor):
        await executor.start()
        assert executor._running is True
        await executor.stop()
        assert executor._running is False

    def test_list_jobs_by_status(self, executor, fake_tool, sample_ticket):
        job = executor.enqueue(sample_ticket, fake_tool, "/tmp")
        executor.cancel_job(job.id)
        queued = executor.list_jobs(status=JobStatus.QUEUED)
        cancelled = executor.list_jobs(status=JobStatus.CANCELLED)
        assert len(queued) == 0
        assert len(cancelled) == 1
