from __future__ import annotations

import unittest
from contextlib import contextmanager
from types import SimpleNamespace
from unittest.mock import patch
from uuid import uuid4

from web.software_followups import claim_followup


class _Result:
    def __init__(self, value=None):
        self.value = value

    def scalar_one_or_none(self):
        return self.value


class _Session:
    def __init__(self, results):
        self.results = list(results)
        self.added = []
        self.statements = []

    def execute(self, statement):
        self.statements.append(statement)
        return _Result(self.results.pop(0) if self.results else None)

    def add(self, row):
        self.added.append(row)


def _job(action: str, *, status: str = "succeeded"):
    job_id = uuid4()
    task_id = uuid4()
    return SimpleNamespace(
        job_id=job_id,
        task_id=task_id,
        user_id=uuid4(),
        capability="origin.plot@v2",
        status=status,
        followup_status="pending",
        completion_action=action,
        error={},
        artifact_manifest=[
            {
                "artifact_id": str(uuid4()),
                "filename": "figure.png",
                "path": f"origin/{job_id}/figure.png",
            },
            {
                "artifact_id": None,
                "filename": "plot-spec.json",
                "path": f"origin/{job_id}/.meta/plot-spec.json",
            },
        ],
    )


class SoftwareFollowupTests(unittest.TestCase):
    def test_report_claim_persists_fixed_assistant_message_with_artifacts(self):
        job = _job("report")
        task = SimpleNamespace(task_id=job.task_id, run_status="idle")
        session = _Session([job.task_id, task, job])

        @contextmanager
        def scope():
            yield session

        with (
            patch("web.software_followups.session_scope", scope),
            patch("web.software_followups.allocate_message_idx", return_value=7),
        ):
            claim = claim_followup(job.job_id)

        self.assertEqual(claim.action, "report")
        self.assertEqual(job.followup_status, "completed")
        self.assertEqual(len(session.added), 1)
        message = session.added[0]
        self.assertEqual(message.payload["role"], "assistant")
        self.assertIn(str(job.job_id), message.payload["content"])
        self.assertEqual(len(message.artifact_refs), 1)
        self.assertEqual(message.artifact_refs[0]["label"], "figure.png")

    def test_analyze_claim_persists_internal_turn_and_locks_task(self):
        job = _job("analyze")
        task = SimpleNamespace(task_id=job.task_id, run_status="idle")
        session = _Session([job.task_id, task, job, None])

        @contextmanager
        def scope():
            yield session

        with (
            patch("web.software_followups.session_scope", scope),
            patch("web.software_followups.allocate_message_idx", return_value=8),
        ):
            claim = claim_followup(job.job_id)

        self.assertEqual(claim.action, "analyze")
        self.assertEqual(job.followup_status, "running")
        self.assertEqual(session.added[0].payload["role"], "user")
        self.assertIn("software_job_status", session.added[0].payload["content"])
        self.assertGreaterEqual(len(session.statements), 4)

    def test_busy_task_leaves_followup_pending(self):
        job = _job("analyze")
        task = SimpleNamespace(task_id=job.task_id, run_status="running")
        session = _Session([job.task_id, task])

        @contextmanager
        def scope():
            yield session

        with patch("web.software_followups.session_scope", scope):
            claim = claim_followup(job.job_id)

        self.assertIsNone(claim)
        self.assertEqual(job.followup_status, "pending")
        self.assertFalse(session.added)

    def test_failed_job_persists_fixed_assistant_message_without_artifacts(self):
        job = _job("analyze", status="failed")
        job.error = {"code": "COLUMN_MISSING", "detail": "  Y2\n does not exist  "}
        task = SimpleNamespace(task_id=job.task_id, run_status="idle")
        session = _Session([job.task_id, task, job])

        @contextmanager
        def scope():
            yield session

        with (
            patch("web.software_followups.session_scope", scope),
            patch("web.software_followups.allocate_message_idx", return_value=9),
        ):
            claim = claim_followup(job.job_id)

        self.assertEqual(claim.action, "report")
        self.assertEqual(job.followup_status, "completed")
        message = session.added[0]
        self.assertEqual(message.payload["role"], "assistant")
        self.assertIn("专业软件任务执行失败", message.payload["content"])
        self.assertIn("Y2 does not exist", message.payload["content"])
        self.assertEqual(message.artifact_refs, [])

    def test_cancelled_job_persists_fixed_assistant_message(self):
        job = _job("analyze", status="cancelled")
        task = SimpleNamespace(task_id=job.task_id, run_status="idle")
        session = _Session([job.task_id, task, job])

        @contextmanager
        def scope():
            yield session

        with (
            patch("web.software_followups.session_scope", scope),
            patch("web.software_followups.allocate_message_idx", return_value=10),
        ):
            claim = claim_followup(job.job_id)

        self.assertEqual(claim.action, "report")
        self.assertIn("专业软件任务已取消", session.added[0].payload["content"])

    def test_busy_task_leaves_failed_notification_pending(self):
        job = _job("report", status="failed")
        task = SimpleNamespace(task_id=job.task_id, run_status="running")
        session = _Session([job.task_id, task])

        @contextmanager
        def scope():
            yield session

        with patch("web.software_followups.session_scope", scope):
            claim = claim_followup(job.job_id)

        self.assertIsNone(claim)
        self.assertEqual(job.followup_status, "pending")
        self.assertFalse(session.added)


if __name__ == "__main__":
    unittest.main()
