from pathlib import Path

from dual_agent.adapters.base import AgentRequest, AgentResult
from dual_agent.adapters.fake import FakeAgentAdapter
from dual_agent.adapters.parse import classify_failure, parse_codex_stream
from dual_agent.domain import (
    ErrorCategory,
    PlanArtifact,
    StageTestReport,
    TaskSpec,
    TaskState,
)
from dual_agent.persistence import TaskStore
from dual_agent.services import Orchestrator


PLAN = '{"schema_version":1,"goal":"g","acceptance_criteria":["ok"]}'


class DirectWorkspace:
    def create(self, task_id: str, repo_path: Path):
        return repo_path, None


def build(tmp_path, planner=None, test_runner=lambda task, path: True):
    repo = tmp_path / "repo"
    repo.mkdir()
    orchestrator = Orchestrator(
        TaskStore(tmp_path / "runtime"),
        planner or FakeAgentAdapter({"PLAN": PLAN}),
        FakeAgentAdapter({}),
        DirectWorkspace(),
        test_runner,
    )
    return orchestrator, orchestrator.create_task(TaskSpec(repo_path=repo, goal="g"))


def test_codex_credit_exhaustion_is_quota_not_protocol():
    message = "Your workspace is out of credits. Ask your workspace owner to refill."

    assert classify_failure(message) == "QUOTA"
    result = parse_codex_stream("", PlanArtifact, message)
    assert result.status == "QUOTA"
    assert result.error == message


def test_recover_retries_the_stage_that_failed_and_archives_failure(tmp_path):
    orchestrator, task = build(tmp_path)
    reviewing = orchestrator._save(
        task,
        state=TaskState.REVIEW,
        last_successful_stage=TaskState.IMPLEMENT.value,
    )
    stopped = orchestrator._needs_human(
        reviewing,
        "Your workspace is out of credits",
        ErrorCategory.QUOTA,
    )
    failure = orchestrator.store.task_dir(task.task_id) / "failure.json"
    assert failure.is_file()

    resumed = orchestrator.recover(stopped.task_id)

    assert resumed.state is TaskState.REVIEW
    assert resumed.failed_stage is None
    assert not failure.exists()
    history = orchestrator.store.task_dir(task.task_id) / "failure-history.jsonl"
    assert history.is_file()
    assert '"category": "QUOTA"' in history.read_text(encoding="utf-8")


def test_finished_agent_stage_clears_every_registered_pid(tmp_path):
    class StartedPlanner:
        def run(self, request: AgentRequest, on_start=None) -> AgentResult:
            on_start(4242)
            return AgentResult(
                "SUCCESS",
                artifact=PlanArtifact(goal="g", acceptance_criteria=["ok"]),
            )

    orchestrator, task = build(tmp_path, planner=StartedPlanner())
    task = orchestrator.advance(task.task_id)
    assert task.state is TaskState.PLAN

    result = orchestrator.advance(task.task_id)

    assert result.active_run_id is None
    assert result.active_run_pid is None
    assert result.active_run_pids == []


def test_three_identical_test_failures_stop_before_cycle_budget(tmp_path):
    report = StageTestReport(
        command="pytest",
        exit_code=1,
        duration_ms=1,
        stdout_path="",
        stderr_path="",
        passed=False,
        failed=2,
    )
    orchestrator, task = build(tmp_path, test_runner=lambda task, path: report)
    testing = orchestrator._save(task, state=TaskState.TEST)

    first = orchestrator._run_tests(testing)
    second = orchestrator._run_tests(orchestrator._save(first, state=TaskState.TEST, cycle=1))
    third = orchestrator._run_tests(orchestrator._save(second, state=TaskState.TEST, cycle=2))

    assert third.state is TaskState.NEEDS_HUMAN
    assert third.error_category is ErrorCategory.STALLED
    assert "repeated 3 times" in third.error
    assert third.cycle < third.limits.max_fix_cycles


def test_done_detail_reports_code_and_deployment_separately(tmp_path):
    import subprocess
    from fastapi.testclient import TestClient
    from dual_agent.api import create_app
    from dual_agent.domain import TaskRecord

    repo = tmp_path / "git-repo"
    repo.mkdir()
    subprocess.run(["git", "init", str(repo)], check=True, capture_output=True)
    subprocess.run(["git", "-C", str(repo), "config", "user.email", "test@example.com"], check=True)
    subprocess.run(["git", "-C", str(repo), "config", "user.name", "Test"], check=True)
    (repo / "tracked.txt").write_text("baseline\n", encoding="utf-8")
    subprocess.run(["git", "-C", str(repo), "add", "tracked.txt"], check=True)
    subprocess.run(["git", "-C", str(repo), "commit", "-m", "baseline"], check=True, capture_output=True)
    baseline = subprocess.run(["git", "-C", str(repo), "rev-parse", "HEAD"], check=True, capture_output=True, text=True).stdout.strip()

    store = TaskStore(tmp_path / "runtime-detail")
    store.create(TaskRecord.new("done-task", repo, "g", state=TaskState.DONE, worktree_path=repo, baseline_commit=baseline))

    class Service:
        def get_task(self, task_id):
            return store.load(task_id)

    service = Service()
    service.store = store
    detail = TestClient(create_app(service)).get("/tasks/done-task/detail").json()
    assert detail["completion"] == {
        "task_complete": True,
        "code_changes": False,
        "deployment_status": "not_tracked",
        "label": "任务完成（未产生代码变更）",
    }


def test_web_explains_done_is_not_deployment():
    from dual_agent.web import INDEX_HTML

    assert "任务完成（未产生代码变更）" in INDEX_HTML or "completion.label" in INDEX_HTML
    assert "部署状态：系统未跟踪" in INDEX_HTML