"""비인간 인물에게 「모든 인물은 인간」이 나가지 않는다 (2026-09-20 사용자 지시).

> "로봇(혹은 인간관 비슷하지만, 외계인등등등등) 인간 규칙 배선부터 고쳐"

## 무엇이 있었나 — 이 화 records 전수 실측

`human_form`(「이야기가 비인간을 명시하지 않는 한 모든 인물은 실제 인간이다」)과
`expression_realism`(「눈꺼풀·얼굴 근육으로 감정을 연기하라」)이 **로봇에게도
그대로 나갔다**: 몸=신원 인물이 실린 84샷 중 **83샷**, 찰리가 실린 77샷은 **전부**.

★값은 **이미 있었다** — `outlook_phase1` 이 `body_identity_chars` 를 구조 칸으로
답하고 `app/core/body_identity.py` 가 읽는다. 소비처가 **참조 고르기뿐**이라
문안으로는 한 줄도 안 갔다. 이 시험은 그 배선을 잠근다.

★**조립부가 아니라 나가는 글**에서 잰다.
"""
import pytest

from app.modules.pipeline.still_recipe import build_still_prompt

HUMAN_RULE = "EVERY CHARACTER IS A HUMAN BEING"
ACTED_RULE = "EXPRESSIONS ARE ACTED"
NONHUMAN = "NOT HUMAN"


def _prompt(char_names, nonhuman_names=(), **kw):
    return build_still_prompt(
        shot_desc="a man stands", place_text="a room",
        time_of_day_en="night", world_anchor="",
        prompt_version="1", bg_only=False, prev_used=False,
        char_names=list(char_names), nonhuman_names=list(nonhuman_names),
        **kw)


def test_all_human_shot_is_unchanged():
    """사람만 나오는 샷은 **종전 그대로** — 인간 규칙이 그대로 나간다."""
    p = _prompt(["현우 (18세 남성)"])
    assert HUMAN_RULE in p
    assert ACTED_RULE in p
    assert NONHUMAN not in p


def test_passing_nothing_is_byte_identical():
    """인자를 안 주면 **한 글자도** 안 바뀐다 — 기존 소비자 보호."""
    base = build_still_prompt(
        shot_desc="a man stands", place_text="a room",
        time_of_day_en="night", world_anchor="",
        prompt_version="1", bg_only=False, prev_used=False,
        char_names=["현우"])
    same = _prompt(["현우"])
    assert base == same


def test_mixed_shot_keeps_the_human_rule_and_names_the_exception():
    """사람과 로봇이 **같이** 있으면 인간 규칙은 남고 예외만 적는다.

    ★규칙을 통째로 빼면 **사람 쪽** 보호(덩어리·실루엣 금지)가 사라진다.
    """
    p = _prompt(["현우 (18세)", "찰리 (로봇)"], ["찰리"])
    assert HUMAN_RULE in p, "사람이 섞였는데 인간 규칙이 빠졌다"
    assert ACTED_RULE in p
    assert NONHUMAN in p, "예외를 안 적었다"
    assert "찰리" in p.split(NONHUMAN)[1][:200], "예외 절이 그 인물을 안 가리킨다"


def test_all_nonhuman_shot_drops_both_human_clauses():
    """등장 인물이 **전부** 비인간이면 두 절을 뺀다 — 가리킬 사람이 없다."""
    p = _prompt(["찰리 (로봇)", "B-200 (로봇)"], ["찰리", "B-200"])
    assert HUMAN_RULE not in p, "로봇만 있는 샷에 인간 규칙이 나갔다"
    assert ACTED_RULE not in p, "로봇만 있는 샷에 연기 규칙이 나갔다"
    assert NONHUMAN in p
    for n in ("찰리", "B-200"):
        assert n in p


def test_the_exception_clause_puts_the_sheet_first():
    """★축은 **정본 우선**이지 「사람 눈 금지」가 아니다 (Codex 감사).

    `body_identity` 는 「몸이 곧 신원」이라는 축이고 「이 인물에게 인간의
    눈이 없다」는 뜻이 아니다 — **사이보그는 인간의 눈을 가질 수 있다.**
    그러니 절은 먼저 「그 정본이 정한 해부·재질대로」를 말하고, 금지는
    거기서 파생되어야 한다(「정본이 주지 않은 사람 눈을 더하지 마라」).
    단정형("no human eyes")으로 쓰면 정본이 인간의 눈을 준 인물에서 틀린다.
    """
    p = _prompt(["찰리 (로봇)"], ["찰리"])
    seg = p.split(NONHUMAN)[1]
    low = seg.lower()
    assert "reference sheet" in low or "that sheet" in low, (
        "무엇을 보고 그릴지가 없다")
    assert "anatomy" in low, "해부를 정본에 맡기는 말이 없다"
    # 금지는 **조건부**여야 한다 — 정본이 안 준 것만.
    i = low.find("human eyes")
    assert i > 0, "사람 눈 이야기가 없다"
    assert "not give" in low[i:i + 220] or "does not" in low[i:i + 220], (
        "사람 눈 금지가 무조건이다 — 정본이 준 경우를 막는다")


@pytest.mark.parametrize("guidance", ["", "17"])
def test_both_branches_carry_the_clause(guidance):
    """grok 컴팩트 갈래도 같은 계약을 쓴다 — 한쪽만 고치면 그쪽에 옛 규칙이 남는다."""
    p = _prompt(["찰리 (로봇)"], ["찰리"], guidance_version=guidance)
    assert NONHUMAN in p
    assert HUMAN_RULE not in p


def test_step_fingerprint_folds_the_pack():
    """팩을 고쳐도 안 내려가는 사고를 막는다 — selector 와 내용을 둘 다 접는다."""
    import inspect

    from app.core.steps import image_steps

    src = inspect.getsource(image_steps)
    assert "nonhuman_cast_pack" in src
    assert "nonhuman_cast_pack_content" in src


def test_a_human_looking_android_is_not_stripped():
    """★구체 반례 — **사람처럼 보이는 몸**을 가진 인물 (Codex 감사 BLOCK).

    이 화의 C44 정본은 이렇게 적혀 있다:

        「젊은 한국인 여성처럼 보이는 인간형 로봇.
         **사람과 같은 얼굴 표면**과 자연스러운 검은 머리.」

    ★이 시험이 태우는 것은 **이름과 절의 계약**이다 — C44 정본 전문을
     실제 입력으로 넣지는 않는다(그 전문은 위 인용이 전부다).

    C44 는 `body_identity_chars` 에 **들어 있다** — 몸이 곧 신원이기
    때문이지 인간의 얼굴이 없어서가 아니다. 그러니 이 절이
    「사람 눈·피부 금지」를 **단정**하면 찰리의 사람 눈을 막으려다
    **C44 의 정본을 훼손한다**.

    절은 두 가지를 다 해야 한다:
      · 정본이 준 인간형 외피는 **그대로** (금지가 무조건이면 안 된다)
      · 정본이 **안 준** 인간 특징은 더하지 않는다
    """
    p = _prompt(["젊은 아시아계 여자 로봇 (인간형)"],
                ["젊은 아시아계 여자 로봇"])
    seg = p.split(NONHUMAN)[1]
    low = seg.lower()
    # ① 「사람처럼 보이는 몸」이 이 절의 사정거리 안이어야 한다
    assert "looks human" in low or "look human" in low, (
        "사람처럼 보이는 몸을 절이 덮지 않는다 — 기계 몸만 말한다")
    # ② 금지는 반드시 **정본을 조건으로** 걸려 있어야 한다
    i = low.find("human eyes")
    assert i > 0
    tail = low[i:i + 220]
    assert "not give" in tail or "does not" in tail, (
        "정본이 인간형 얼굴을 준 인물까지 막는 무조건 금지다")
    # ③ 정본이 우선이라는 말이 절 안에 있어야 한다
    assert "nothing here overrides" in low or "exactly as it describes" in low, (
        "정본 우선이 명시되지 않았다")


def test_compact_branch_keeps_the_same_conditional(_=None):
    """★grok 컴팩트판에도 **조건부 정본 우선**이 남아야 한다 (Codex 감사).

    짧게 줄이다 보면 「no human eyes」만 남고 조건이 떨어지기 쉽다 —
    그러면 컴팩트 갈래에서만 C44 같은 인물이 훼손된다.
    """
    p = _prompt(["젊은 아시아계 여자 로봇 (인간형)"],
                ["젊은 아시아계 여자 로봇"], guidance_version="17")
    seg = p.split(NONHUMAN)[1].lower()
    assert "sheet" in seg, "컴팩트판에 정본을 가리키는 말이 없다"
    assert "anatomy" in seg, "컴팩트판에 해부를 정본에 맡기는 말이 없다"
    i = seg.find("human eyes")
    assert i > 0
    tail = seg[i:i + 200]
    assert "not give" in tail or "does not" in tail, (
        "컴팩트판의 사람 눈 금지가 무조건이다")
