import json
from unittest.mock import MagicMock, patch

from armorer.core import ArmorerCoreHandler


@patch("armorer.core.BaseAgent")
@patch("armorer.core.AppRegistry")
def test_core_uses_orchestrator_executor_and_verifier_for_background_jobs(mock_registry_cls, mock_agent_cls, tmp_path):
    config_dir = tmp_path / ".armorer"
    config_dir.mkdir()
    inbox = config_dir / "inbox"
    outbox = config_dir / "outbox"
    status = config_dir / "status"
    for path in (inbox, outbox, status):
        path.mkdir()

    registry = mock_registry_cls.return_value
    registry.list_apps.return_value = []

    orchestrator = MagicMock()
    executor = MagicMock()
    verifier = MagicMock()
    mock_agent_cls.side_effect = [orchestrator, executor, verifier]

    orchestrator.run.return_value = {
        "route": "execute",
        "task_brief": "Install openclaw",
        "execution_mode": "async",
        "acceptance_criteria": ["A background install job is started and trackable."],
    }
    executor.run.return_value = {
        "message": "Installing openclaw...",
        "ui_action": {
            "type": "track_job",
            "job_id": "job-123",
            "message": "Installing openclaw...",
        },
    }
    verifier.run.return_value = {
        "passed": True,
        "user_message": "Installing openclaw... Started in the background.",
        "evidence": ["Job job-123 exists and is trackable."],
        "retryable": False,
    }

    handler = ArmorerCoreHandler(config_dir)
    task_path = inbox / "task.json"
    task_path.write_text(json.dumps({"id": "task-1", "prompt": "install openclaw", "source": "cli"}))

    handler._process_inbox_task(str(task_path))

    assert orchestrator.run.call_args.kwargs["user_input"] == [{"role": "user", "content": "install openclaw"}]
    assert "install openclaw" in executor.run.call_args.kwargs["user_input"].lower()
    assert "track_job" in executor.init_agent.call_args.kwargs["system_prompt"]
    assert "Do not trust executor claims" in verifier.init_agent.call_args.kwargs["system_prompt"]
    assert "prove success or failure with observed evidence" in verifier.init_agent.call_args.kwargs["system_prompt"]
    assert "inspect logs or recent outputs" in verifier.init_agent.call_args.kwargs["system_prompt"]
    assert verifier.run.call_args.kwargs["structured_output_model"].__name__ == "VerificationResult"

    result = json.loads((outbox / "task-1.json").read_text())
    payload = json.loads(result["response"])
    assert "background" in payload["message"].lower()
    assert payload["ui_action"]["type"] == "track_job"
    assert payload["ui_action"]["job_id"] == "job-123"


@patch("armorer.core.BaseAgent")
@patch("armorer.core.AppRegistry")
def test_core_persists_session_history_across_direct_chat_tasks(mock_registry_cls, mock_agent_cls, tmp_path):
    config_dir = tmp_path / ".armorer"
    config_dir.mkdir()
    inbox = config_dir / "inbox"
    outbox = config_dir / "outbox"
    status = config_dir / "status"
    for path in (inbox, outbox, status):
        path.mkdir()

    registry = mock_registry_cls.return_value
    registry.list_apps.return_value = []

    run_inputs = []
    orchestrator_one = MagicMock()
    orchestrator_two = MagicMock()
    mock_agent_cls.side_effect = [orchestrator_one, orchestrator_two]

    def fake_run(*, user_input, status_file=None, structured_output_model=None):
        del status_file, structured_output_model
        run_inputs.append(user_input)
        return {"route": "direct_answer", "user_message": f"echo:{len(run_inputs)}"}

    orchestrator_one.run.side_effect = fake_run
    orchestrator_two.run.side_effect = fake_run

    handler = ArmorerCoreHandler(config_dir)
    session_id = "chat-123"

    task1 = inbox / "task1.json"
    task1.write_text(json.dumps({"id": "task-1", "session_id": session_id, "prompt": "hi", "source": "cli"}))
    handler._process_inbox_task(str(task1))

    task2 = inbox / "task2.json"
    task2.write_text(json.dumps({"id": "task-2", "session_id": session_id, "prompt": "what agents?", "source": "cli"}))
    handler._process_inbox_task(str(task2))

    assert run_inputs[0] == [{"role": "user", "content": "hi"}]
    assert run_inputs[1] == [
        {"role": "user", "content": "hi"},
        {"role": "assistant", "content": "echo:1"},
        {"role": "user", "content": "what agents?"},
    ]

    session_file = config_dir / "sessions" / f"{session_id}.json"
    saved = json.loads(session_file.read_text())
    assert saved["messages"][-1] == {"role": "assistant", "content": "echo:2"}


@patch("armorer.core.BaseAgent")
@patch("armorer.core.AppRegistry")
def test_core_retries_executor_once_when_verifier_rejects_incomplete_output(mock_registry_cls, mock_agent_cls, tmp_path):
    config_dir = tmp_path / ".armorer"
    config_dir.mkdir()
    inbox = config_dir / "inbox"
    outbox = config_dir / "outbox"
    status = config_dir / "status"
    for path in (inbox, outbox, status):
        path.mkdir()

    registry = mock_registry_cls.return_value
    registry.list_apps.return_value = []

    orchestrator = MagicMock()
    executor = MagicMock()
    verifier = MagicMock()
    mock_agent_cls.side_effect = [orchestrator, executor, verifier]

    orchestrator.run.return_value = {
        "route": "execute",
        "task_brief": "Check whether openclaw is running",
        "execution_mode": "sync",
        "acceptance_criteria": ["Return a concrete verified status for openclaw."],
    }
    executor.run.side_effect = [
        {"message": "I'll take a closer look:"},
        {"message": "OpenClaw is running."},
    ]
    verifier.run.side_effect = [
        {
            "passed": False,
            "user_message": "The previous reply did not answer the request.",
            "failed_criteria": ["No concrete result was provided."],
            "evidence": ["Observed executor reply was only a lead-in and did not include a status result."],
            "retryable": True,
        },
        {
            "passed": True,
            "user_message": "OpenClaw is running.",
            "evidence": ["Observed a running status check."],
            "retryable": False,
        },
    ]

    handler = ArmorerCoreHandler(config_dir)
    task_path = inbox / "task.json"
    task_path.write_text(json.dumps({"id": "task-1", "prompt": "is openclaw running?", "source": "cli"}))

    handler._process_inbox_task(str(task_path))

    assert executor.run.call_count == 2
    retry_input = executor.run.call_args_list[1].kwargs["user_input"]
    assert "no concrete result was provided" in retry_input.lower()
    assert "lead-in" in retry_input.lower()

    result = json.loads((outbox / "task-1.json").read_text())
    payload = json.loads(result["response"])
    assert payload["message"] == "OpenClaw is running."


@patch("armorer.core.BaseAgent")
@patch("armorer.core.AppRegistry")
def test_core_returns_executor_track_job_payload_if_async_verifier_raises(mock_registry_cls, mock_agent_cls, tmp_path):
    config_dir = tmp_path / ".armorer"
    config_dir.mkdir()
    inbox = config_dir / "inbox"
    outbox = config_dir / "outbox"
    status = config_dir / "status"
    for path in (inbox, outbox, status):
        path.mkdir()

    registry = mock_registry_cls.return_value
    registry.list_apps.return_value = []

    orchestrator = MagicMock()
    executor = MagicMock()
    verifier = MagicMock()
    mock_agent_cls.side_effect = [orchestrator, executor, verifier]

    orchestrator.run.return_value = {
        "route": "execute",
        "task_brief": "Uninstall goliclaw",
        "execution_mode": "async",
        "acceptance_criteria": ["A background uninstall job is started and trackable."],
    }
    executor.run.return_value = {
        "message": "Uninstalling goliclaw...",
        "ui_action": {
            "type": "track_job",
            "job_id": "job-456",
            "message": "Uninstalling goliclaw...",
        },
    }
    verifier.run.side_effect = RuntimeError("Max turns (20) exceeded")

    handler = ArmorerCoreHandler(config_dir)
    task_path = inbox / "task.json"
    task_path.write_text(json.dumps({"id": "task-1", "prompt": "uninstall goliclaw", "source": "cli"}))

    handler._process_inbox_task(str(task_path))

    result = json.loads((outbox / "task-1.json").read_text())
    payload = json.loads(result["response"])
    assert payload["message"] == "Uninstalling goliclaw..."
    assert payload["ui_action"]["type"] == "track_job"
    assert payload["ui_action"]["job_id"] == "job-456"
