"""Tests for the job executor."""

from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import AsyncMock, patch

import pytest

from fliiq.runtime.agent.loop import AgentResult
from fliiq.runtime.llm.providers import LLMProvider, LLMResponse
from fliiq.runtime.scheduler.executor import _build_delivery_hint, execute_job, template_prompt
from fliiq.runtime.scheduler.loader import load_job, save_job
from fliiq.runtime.scheduler.models import DeliveryConfig, JobDefinition, TriggerConfig
from fliiq.runtime.scheduler.run_log import load_run_logs


def _make_job(name: str = "test-job", prompt: str = "Do the thing") -> JobDefinition:
    return JobDefinition(
        name=name,
        trigger=TriggerConfig(type="cron", schedule="0 9 * * *"),
        prompt=prompt,
    )


def _mock_agent_result(text: str = "Done.", iterations: int = 3) -> AgentResult:
    return AgentResult(
        messages=[{"role": "assistant", "content": text}],
        final_text=text,
        iterations=iterations,
        stop_reason="end_turn",
    )


def _mock_llm_response(text: str = "Done.") -> LLMResponse:
    return LLMResponse(
        content=text,
        model="test-model",
        provider=LLMProvider.ANTHROPIC,
        stop_reason="end_turn",
        raw_content=[{"type": "text", "text": text}],
    )


# --- template_prompt ---


def _wrap(value: str) -> str:
    """Expected external_message wrapping for webhook payloads."""
    return f'<external_message source="webhook">\n{value}\n</external_message>'


def test_template_prompt_simple():
    result = template_prompt("Hello {{name}}", {"name": "world"})
    assert result == f"Hello {_wrap('world')}"


def test_template_prompt_nested():
    result = template_prompt("Ticket {{issue.key}} moved", {"issue": {"key": "PROJ-123"}})
    assert result == f"Ticket {_wrap('PROJ-123')} moved"


def test_template_prompt_no_payload():
    result = template_prompt("Hello {{name}}", None)
    assert result == "Hello {{name}}"


def test_template_prompt_missing_key():
    result = template_prompt("Hello {{missing}}", {"other": "value"})
    assert result == "Hello {{missing}}"


def test_template_prompt_multiple():
    result = template_prompt("{{a}} and {{b}}", {"a": "1", "b": "2"})
    assert result == f"{_wrap('1')} and {_wrap('2')}"


# --- execute_job ---


async def test_execute_job_success(tmp_path):
    """Job executor calls agent_loop and saves state + run log."""
    job = _make_job()
    save_job(job, tmp_path)

    with (
        patch("fliiq.runtime.scheduler.executor.setup_agent_resources") as mock_setup,
        patch("fliiq.runtime.scheduler.executor.agent_loop") as mock_loop,
    ):
        # Mock setup to return minimal resources
        mock_resources = AsyncMock()
        mock_resources.soul = "Test soul"
        mock_resources.user_soul = None
        mock_resources.skill_info = []
        mock_resources.memory_context = None
        mock_resources.llm = AsyncMock()
        mock_resources.tools = AsyncMock()
        mock_resources.tools.get_tool_definitions = lambda: []
        mock_resources.project_root = tmp_path
        mock_setup.return_value = mock_resources

        mock_loop.return_value = _mock_agent_result()

        entry = await execute_job(job, tmp_path, tmp_path, None)

    assert entry.status == "success"
    assert entry.iterations == 3
    assert entry.stop_reason == "end_turn"
    assert entry.duration_ms is not None

    # Verify state was updated
    loaded = load_job(tmp_path / "test-job.yaml")
    assert loaded.state.last_status == "success"
    assert loaded.state.run_count == 1

    # Verify run log was saved
    logs = load_run_logs("test-job", tmp_path)
    assert len(logs) == 1
    assert logs[0].status == "success"


async def test_execute_job_agent_error(tmp_path):
    """Job executor handles agent_loop exceptions gracefully."""
    job = _make_job()
    save_job(job, tmp_path)

    with (
        patch("fliiq.runtime.scheduler.executor.setup_agent_resources") as mock_setup,
        patch("fliiq.runtime.scheduler.executor.agent_loop") as mock_loop,
    ):
        mock_resources = AsyncMock()
        mock_resources.soul = "Test soul"
        mock_resources.user_soul = None
        mock_resources.skill_info = []
        mock_resources.memory_context = None
        mock_resources.llm = AsyncMock()
        mock_resources.tools = AsyncMock()
        mock_resources.tools.get_tool_definitions = lambda: []
        mock_resources.project_root = tmp_path
        mock_setup.return_value = mock_resources

        mock_loop.side_effect = RuntimeError("LLM exploded")

        entry = await execute_job(job, tmp_path, tmp_path, None)

    assert entry.status == "error"
    assert "LLM exploded" in entry.error


async def test_execute_job_setup_error(tmp_path):
    """Job executor handles setup failures gracefully."""
    job = _make_job()
    save_job(job, tmp_path)

    with patch("fliiq.runtime.scheduler.executor.setup_agent_resources") as mock_setup:
        mock_setup.side_effect = FileNotFoundError("SOUL.md not found")

        entry = await execute_job(job, tmp_path, tmp_path, None)

    assert entry.status == "error"
    assert "Setup failed" in entry.error


# --- _build_delivery_hint ---


def test_delivery_hint_email():
    delivery = DeliveryConfig(type="email", to="user@example.com")
    hint = _build_delivery_hint(delivery)
    assert "send_email" in hint
    assert "user@example.com" in hint


def test_delivery_hint_sms():
    delivery = DeliveryConfig(type="sms", to="+14155551234")
    hint = _build_delivery_hint(delivery)
    assert "send_sms" in hint
    assert "+14155551234" in hint


def test_delivery_hint_none():
    assert _build_delivery_hint(None) == ""


def test_delivery_hint_unknown_type():
    delivery = DeliveryConfig(type="slack", channel="#general")
    hint = _build_delivery_hint(delivery)
    assert "not yet supported" in hint
