# Copyright (c) 2023-present Pi Dash Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

import pytest

from pi_dash.orchestration.done_signal import (
    DoneSignalError,
    ingest_into_run,
    parse,
)
from pi_dash.runner.models import AgentRun, AgentRunStatus


VALID_BODY = """
Some preamble chatter that the agent printed before the fence.

```pi-dash-done
{
  "status": "completed",
  "summary": "made the button blue",
  "state_transition": {"requested_group": "completed", "reason": "implementation done"},
  "changes": {
    "branch": "feat/blue-button",
    "commits": ["abc123"],
    "files_touched": ["web/button.tsx"],
    "pr_url": null
  },
  "validation": {"acceptance_all_met": true, "ran": ["pnpm test"], "notes": null},
  "progress": {
    "phase": "implementing",
    "checkpoints": {
      "investigation_complete": true,
      "design_choice_recorded": true,
      "implementation_complete": true,
      "validation_complete": true,
      "pr_opened": "n/a",
      "review_feedback_addressed": "n/a"
    }
  },
  "autonomy": {
    "score": 1,
    "type": "none",
    "reason": "clear local choice",
    "question_for_human": null,
    "safe_to_continue": true
  },
  "blockers": []
}
```
"""


@pytest.mark.unit
def test_parse_valid_fence():
    signal = parse(VALID_BODY)
    assert signal.status == "completed"
    assert signal.payload["changes"]["branch"] == "feat/blue-button"
    assert signal.payload["progress"]["phase"] == "implementing"


@pytest.mark.unit
def test_parse_missing_fence_raises():
    with pytest.raises(DoneSignalError):
        parse("no fence here")


@pytest.mark.unit
def test_parse_invalid_json_raises():
    body = "```pi-dash-done\n{not-json}\n```"
    with pytest.raises(DoneSignalError):
        parse(body)


@pytest.mark.unit
def test_parse_rejects_unknown_status():
    body = '```pi-dash-done\n{"status": "bogus"}\n```'
    with pytest.raises(DoneSignalError):
        parse(body)


@pytest.mark.unit
def test_parse_tolerates_embedded_backticks_in_string_value():
    """``summary`` is free-form prose; an agent quoting triple backticks inside
    the JSON value must not terminate the fence early."""
    body = (
        "```pi-dash-done\n"
        '{"status": "completed", "summary": "used ``` fences in the PR body"}\n'
        "```"
    )
    signal = parse(body)
    assert signal.status == "completed"
    assert "```" in signal.payload["summary"]


@pytest.mark.unit
def test_ingest_into_run_completed(db, workspace, create_user):
    run = AgentRun.objects.create(
        owner=create_user,
        workspace=workspace,
        prompt="",
        status=AgentRunStatus.RUNNING,
    )
    signal = ingest_into_run(run, VALID_BODY)
    run.refresh_from_db()
    assert signal is not None
    assert run.status == AgentRunStatus.COMPLETED
    assert run.done_payload["status"] == "completed"


@pytest.mark.unit
def test_ingest_into_run_blocked(db, workspace, create_user):
    run = AgentRun.objects.create(
        owner=create_user,
        workspace=workspace,
        prompt="",
        status=AgentRunStatus.RUNNING,
    )
    body = (
        "```pi-dash-done\n"
        '{"status": "blocked", "blockers": ["missing key"]}\n'
        "```"
    )
    ingest_into_run(run, body)
    run.refresh_from_db()
    assert run.status == AgentRunStatus.BLOCKED
    assert run.done_payload["blockers"] == ["missing key"]


@pytest.mark.unit
def test_ingest_malformed_marks_failed(db, workspace, create_user):
    run = AgentRun.objects.create(
        owner=create_user,
        workspace=workspace,
        prompt="",
        status=AgentRunStatus.RUNNING,
    )
    ingest_into_run(run, "agent forgot the fence entirely")
    run.refresh_from_db()
    assert run.status == AgentRunStatus.FAILED
    assert "done-signal parse error" in run.error
    assert run.ended_at is not None


@pytest.mark.unit
def test_ingest_success_clears_prior_error_and_stamps_ended_at(
    db, workspace, create_user
):
    """Regression: a run that was previously marked FAILED via a malformed
    signal must have its error blanked and ended_at stamped when a later
    valid signal is ingested (e.g. after a runner retry)."""
    run = AgentRun.objects.create(
        owner=create_user,
        workspace=workspace,
        prompt="",
        status=AgentRunStatus.RUNNING,
        error="previous parse failure",
    )
    ingest_into_run(run, VALID_BODY)
    run.refresh_from_db()
    assert run.status == AgentRunStatus.COMPLETED
    assert run.error == ""
    assert run.ended_at is not None


# ---------------------------------------------------------------------------
# Paused status — agent yields with a question, expects a follow-up run.
# ---------------------------------------------------------------------------


PAUSED_BODY = """
```pi-dash-done
{
  "status": "paused",
  "summary": "stuck on which API surface to expose",
  "autonomy": {
    "score": 0,
    "type": "blocking",
    "reason": "ambiguous requirement",
    "question_for_human": "Should this be on /api/v1/ or /api/v2/?",
    "safe_to_continue": false
  },
  "blockers": []
}
```
"""


PAUSED_BODY_NO_QUESTION = """
```pi-dash-done
{
  "status": "paused",
  "summary": "stuck",
  "autonomy": {"score": 0, "type": "blocking", "reason": "x"},
  "blockers": []
}
```
"""


@pytest.mark.unit
def test_parse_paused_with_question_succeeds():
    signal = parse(PAUSED_BODY)
    assert signal.status == "paused"
    assert (
        signal.payload["autonomy"]["question_for_human"]
        == "Should this be on /api/v1/ or /api/v2/?"
    )


@pytest.mark.unit
def test_parse_paused_without_question_rejected():
    with pytest.raises(DoneSignalError, match="question_for_human"):
        parse(PAUSED_BODY_NO_QUESTION)


@pytest.mark.unit
def test_ingest_paused_transitions_to_paused_awaiting_input(
    db, create_user, workspace
):
    run = AgentRun.objects.create(
        owner=create_user,
        workspace=workspace,
        prompt="x",
        status=AgentRunStatus.RUNNING,
    )
    signal = ingest_into_run(run, PAUSED_BODY)
    run.refresh_from_db()
    assert signal is not None
    assert run.status == AgentRunStatus.PAUSED_AWAITING_INPUT
    # Paused is non-terminal; ended_at must remain NULL.
    assert run.ended_at is None
    assert run.done_payload["autonomy"]["question_for_human"]
