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

import asyncio
from unittest.mock import patch

import pytest

from proxym.dashboard._tickets_api import _tickets
from proxym.dashboard.tickets import Ticket, TicketStatus, 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_success_moves_ticket_to_in_review(self, executor, fake_tool):
        mgr = _tickets()
        ticket = mgr.create_ticket(Ticket(title="Transition to review"))
        mgr.update_ticket(ticket.id, {"status": TicketStatus.IN_PROGRESS.value})

        job = executor.enqueue(ticket, fake_tool, "/tmp")
        await executor._execute_job(job)

        updated = mgr.get_ticket(ticket.id)
        assert updated is not None
        assert updated.status == TicketStatus.IN_REVIEW
        assert updated.last_execution is not None

    @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
