"""_cherry_unmerged_entry must not flag squash-merged children as missing.

Live false positive (2026-07-02): three children of the S6 cell task were
squash-merged (PRs #176/#185/#190) — their commits sat at the assembled
branch tip, yet ``git cherry`` reported every individual child commit as
unmerged (a squash rewrites N patches into one patch-id) and the assembly
integrity guard refused every legitimate submit_up.

Relief: every commit — including the squash commit — carries the
``[taskid8]`` prefix, so a marker-bearing commit on the parent proves the
child landed. A child with no marker on the parent stays flagged (the
original incident #11 the guard exists for).
"""

from __future__ import annotations

import re
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock
from uuid import uuid4

import pytest
from roboco.services.git import GitService


def _svc_with_git_responses(
    responses: dict[str, SimpleNamespace],
) -> tuple[GitService, list[list[str]]]:
    """GitService with _run_git stubbed by subcommand name; records calls."""
    svc = GitService.__new__(GitService)
    calls: list[list[str]] = []

    async def _run_git(
        _workspace: Path, args: list[str], **_kw: Any
    ) -> SimpleNamespace:
        calls.append(args)
        return responses[args[0]]

    svc_any: Any = svc
    svc_any._run_git = _run_git
    return svc, calls


def _child() -> MagicMock:
    return MagicMock(
        id=uuid4(), branch_name="feature/frontend/root--cell--child", title="t"
    )


@pytest.mark.asyncio
async def test_squash_merged_child_with_task_marker_is_not_flagged() -> None:
    """cherry says unmerged, but the [taskid8] squash commit is on the parent."""
    svc, calls = _svc_with_git_responses(
        {
            "rev-parse": SimpleNamespace(returncode=0, stdout="abc\n"),
            "cherry": SimpleNamespace(returncode=0, stdout="+ aaa\n+ bbb\n"),
            "log": SimpleNamespace(
                returncode=0, stdout="4771bd71 [deadbeef] title (#190)\n"
            ),
        }
    )
    entry = await svc._cherry_unmerged_entry(Path("/tmp"), "parent", _child())
    assert entry is None
    log_call = next(c for c in calls if c[0] == "log")
    assert any("\\[" in arg for arg in log_call)  # grep pattern escapes the bracket


@pytest.mark.asyncio
async def test_genuinely_missing_child_stays_flagged() -> None:
    """No marker commit on the parent → the original #11 catch still fires."""
    child = _child()
    svc, _calls = _svc_with_git_responses(
        {
            "rev-parse": SimpleNamespace(returncode=0, stdout="abc\n"),
            "cherry": SimpleNamespace(returncode=0, stdout="+ aaa\n"),
            "log": SimpleNamespace(returncode=0, stdout=""),
        }
    )
    entry = await svc._cherry_unmerged_entry(Path("/tmp"), "parent", child)
    assert entry == {"task_id": str(child.id)[:8], "title": "t", "unmerged": 1}


@pytest.mark.asyncio
async def test_revert_commit_referencing_child_marker_does_not_relief() -> None:
    """A 'Reverts [CHILDID8] ...' commit references the child but isn't the
    child's squash commit — the marker grep must anchor to the commit prefix
    so a bare reference doesn't hide the child's real unmerged commits.

    The log stub applies ``git log --grep <pat>`` as a real regex search per
    line, so the anchored pattern is exercised behaviorally (an anchored
    ``^...`` excludes the revert; a bare ``[id]`` would match it)."""
    child = _child()
    cid = str(child.id)[:8]
    log_lines = [
        f"4771bd71 [{cid}] real child squash commit (#190)\n",
        f"abc12345 Reverts [{cid}] sibling revert (#200)\n",
    ]
    svc = GitService.__new__(GitService)

    async def _run_git(_ws: Path, args: list[str], **_kw: Any) -> SimpleNamespace:
        if args[0] == "rev-parse":
            return SimpleNamespace(returncode=0, stdout="abc\n")
        if args[0] == "cherry":
            return SimpleNamespace(returncode=0, stdout="+ aaa\n")
        if args[0] == "log":
            pat = args[args.index("--grep") + 1]
            # git log --grep matches the commit message, not the oneline SHA.
            hits = [
                ln
                for ln in log_lines
                if re.search(pat, ln.split(" ", 1)[1] if " " in ln else ln)
            ]
            return SimpleNamespace(returncode=0, stdout="".join(hits))
        return SimpleNamespace(returncode=0, stdout="")

    svc_any: Any = svc
    svc_any._run_git = _run_git

    # Revert-only: anchored grep excludes it → child stays flagged.
    log_lines = [f"abc12345 Reverts [{cid}] sibling revert (#200)\n"]
    entry = await svc._cherry_unmerged_entry(Path("/tmp"), "parent", child)
    assert entry == {"task_id": cid, "title": "t", "unmerged": 1}

    # Real marker present: anchored grep matches → squash-merge relief fires.
    log_lines = [
        f"4771bd71 [{cid}] real child squash commit (#190)\n",
        f"abc12345 Reverts [{cid}] sibling revert (#200)\n",
    ]
    entry = await svc._cherry_unmerged_entry(Path("/tmp"), "parent", child)
    assert entry is None


@pytest.mark.asyncio
async def test_cherry_clean_short_circuits_without_marker_probe() -> None:
    """No + lines from cherry → merged; the log probe is never run."""
    svc, calls = _svc_with_git_responses(
        {
            "rev-parse": SimpleNamespace(returncode=0, stdout="abc\n"),
            "cherry": SimpleNamespace(returncode=0, stdout="- aaa\n"),
        }
    )
    entry = await svc._cherry_unmerged_entry(Path("/tmp"), "parent", _child())
    assert entry is None
    assert not any(c[0] == "log" for c in calls)
