"""records.json 의 refs 가 자산 id 를 나른다.

지금은 bytes 참조가 `<bytes:870689>` 로만 남아 어느 자산인지 모른다.
완주 판 실측: refs 449건 중 248건(55%)이 자산 불명, 대부분이
CHARACTER REFERENCE(222건).
"""
from app.modules.pipeline.multiroll_select import build_ref_records


def test_bytes_ref_keeps_placeholder_and_gains_asset_id():
    labeled = [("CHARACTER REFERENCE — 김선영", b"x" * 100)]
    metas = [{"asset_id": "aaaa-1111", "pipeline_role": "character_ref"}]
    out = build_ref_records(labeled, metas)
    assert out == [{
        "label": "CHARACTER REFERENCE — 김선영",
        "path": "<bytes:100>",          # ★기존 칸은 그대로 — 읽는 데가 있다
        "asset_id": "aaaa-1111",
        "role": "character_ref",
    }]


def test_path_ref_keeps_path():
    labeled = [("LOCATION PHOTOGRAPH", "/a/b/plate.png")]
    out = build_ref_records(labeled, [{"asset_id": None}])
    assert out[0]["path"] == "/a/b/plate.png"
    assert out[0]["asset_id"] is None


def test_missing_metadata_is_tolerated():
    """병렬 목록이 없거나 짧아도 기록은 나와야 한다."""
    labeled = [("A", b"1"), ("B", b"2")]
    assert [r["asset_id"] for r in build_ref_records(labeled, None)] == [None, None]
    assert [r["asset_id"] for r in build_ref_records(labeled, [{"asset_id": "x"}])] \
        == ["x", None]


def test_extra_metadata_is_ignored_not_crashing():
    labeled = [("A", b"1")]
    metas = [{"asset_id": "x"}, {"asset_id": "y"}]
    assert len(build_ref_records(labeled, metas)) == 1


def test_index_alignment_is_positional_not_label_based():
    """같은 라벨이 둘이어도 index 로 갈린다."""
    labeled = [("SAME", b"1"), ("SAME", b"2")]
    metas = [{"asset_id": "first"}, {"asset_id": "second"}]
    out = build_ref_records(labeled, metas)
    assert [r["asset_id"] for r in out] == ["first", "second"]


# ── 등록 자리를 하나도 안 빠뜨렸는지 (Task 10 Step 7-B) ────────────────
from app.modules.pipeline.still_recipe import build_still_refs, ref_source_key


def test_every_attach_role_has_a_registration():
    """★_attach 하는 역할은 신원 등록도 있어야 한다 — 자산이 있는 것만.

    지도가 비면 그 참조의 asset_id 가 조용히 None 이 된다 — 고치려던 결함이
    그대로 남는데 아무도 모른다.

    ★역할은 **AST 로** 센다. 문자열 검색으로 세다가 두 번 틀렸다(9 → 15 →
    실제 13). _attach 첫 인자가 아닌 문자열을 역할로 읽었다.
    """
    import ast
    import re
    from pathlib import Path

    src = (Path(__file__).resolve().parents[2] / "app" / "services"
           / "still_recipe_service.py").read_text(encoding="utf-8")
    tree = ast.parse(src)

    with_asset, without_asset = set(), set()
    for n in ast.walk(tree):
        if not (isinstance(n, ast.Call)
                and getattr(n.func, "id", "") == "_attach"
                and n.args and isinstance(n.args[0], ast.Constant)):
            continue
        role = n.args[0].value
        aid = n.args[2] if len(n.args) > 2 else None
        if isinstance(aid, ast.Constant) and aid.value is None:
            without_asset.add(role)
        else:
            with_asset.add(role)

    # ★등록도 **AST 로** 센다. 정규식(`_register_ref\([^)]*"..."`)은 인자
    #   안의 괄호에서 끊긴다 — `scene_ref_asset_id_map.get(key)` 가 그 예다.
    #   실제로 그 정규식으로 세다가 배선해 둔 역할 넷을 「없다」고 읽었다.
    reg_roles = set()
    for n in ast.walk(tree):
        if isinstance(n, ast.Call) and \
                getattr(n.func, "id", "") == "_register_ref":
            for a in n.args:
                if isinstance(a, ast.Constant) and isinstance(a.value, str):
                    reg_roles.add(a.value)

    # 자산이 원래 없는 역할 — asset_id=None 으로 명시 부착한다. 등록 대상 아님.
    assert without_asset <= {"confined_fp", "era_ref"}, \
        f"자산 없는 역할이 늘었다: {sorted(without_asset)}"

    # ★`lane_canon_master` 는 참조 원본이 `_authority_path` 다. 그 원본은
    #   권위 종류 이름(`bgfirst_group_bg`·`location_plate` 등)으로 이미
    #   등록되므로 **asset_id 는 이어진다** — 역할 라벨만 다르다.
    #   따로 또 등록하면 같은 신원에 두 역할이 되어 fail-closed(None) 로
    #   떨어져 계보가 오히려 끊긴다.
    joined_via_authority = {"lane_canon_master"}

    missing = with_asset - reg_roles - joined_via_authority
    assert not missing, f"신원 등록이 없는 역할: {sorted(missing)}"

    total = with_asset | without_asset
    assert len(total) == 13, (
        f"_attach 역할이 {len(total)}개 — 계획이 센 것은 13개다. "
        f"늘었으면 등록 목록에 보태고, 줄었으면 왜 사라졌는지 볼 것: "
        f"{sorted(total)}")


def test_bgfirst_authority_refs_are_registered_before_branch():
    """★bgfirst 는 plate·prev 를 _run_branch **전에** 등록해야 한다.

    ★줄 번호 비교로는 증명이 안 된다. `_attach_plate` **함수 본문 안**의
    등록도 소스 줄로는 앞이지만 그 함수는 승자 확정 뒤에야 **불린다**.
    정의 위치가 아니라 **실행 경로**를 봐야 한다.
    """
    import ast
    from pathlib import Path

    src = (Path(__file__).resolve().parents[2] / "app" / "services"
           / "still_recipe_service.py").read_text(encoding="utf-8")
    tree = ast.parse(src)

    def _registers(node, role: str) -> bool:
        for n in ast.walk(node):
            if (isinstance(n, ast.Call)
                    and getattr(n.func, "id", "") == "_register_ref"
                    and any(isinstance(a, ast.Constant) and a.value == role
                            for a in n.args)):
                return True
        return False

    def _authority_guards(kind: str):
        out = []
        for n in ast.walk(tree):
            if not isinstance(n, ast.If):
                continue
            dump = ast.dump(n.test)
            if "_authority_kind" in dump and f"'{kind}'" in dump:
                out.append(n)
        return out

    plate_guards = _authority_guards("plate")
    prev_guards = _authority_guards("prev")

    assert plate_guards, '_authority_kind == "plate" 분기를 못 찾았다'
    assert prev_guards, '_authority_kind == "prev" 분기를 못 찾았다'
    assert any(_registers(g, "location_plate") for g in plate_guards), (
        "bgfirst plate 갈래가 _run_branch 전에 location_plate 를 등록하지 "
        "않는다 — 그 후보의 asset_id 가 통째로 빈다")
    assert any(_registers(g, "prev_still") for g in prev_guards), (
        "bgfirst prev 갈래가 prev_still 을 등록하지 않는다")


def test_authority_registration_precedes_branch_refs_build():
    """★등록이 refs 조립보다 앞인지 **역할마다 따로** 본다.

    한 목록으로 합쳐 any(...) 로 보면 prev 가 앞에 있을 때 plate 가 뒤로
    밀려도 통과한다.
    """
    import ast
    from pathlib import Path

    src = (Path(__file__).resolve().parents[2] / "app" / "services"
           / "still_recipe_service.py").read_text(encoding="utf-8")
    tree = ast.parse(src)

    def _guard_reg_lines(kind: str, role: str):
        out = []
        for n in ast.walk(tree):
            if not isinstance(n, ast.If):
                continue
            dump = ast.dump(n.test)
            if "_authority_kind" not in dump or f"'{kind}'" not in dump:
                continue
            for c in ast.walk(n):
                if (isinstance(c, ast.Call)
                        and getattr(c.func, "id", "") == "_register_ref"
                        and any(isinstance(a, ast.Constant) and a.value == role
                                for a in c.args)):
                    out.append(c.lineno)
        return out

    build_lines = [
        n.lineno for n in ast.walk(tree)
        if isinstance(n, ast.Call)
        and getattr(n.func, "id", "") == "build_ab_branch_refs"
    ]
    assert build_lines, "build_ab_branch_refs 호출을 못 찾았다"
    first_build = min(build_lines)

    plate_lines = _guard_reg_lines("plate", "location_plate")
    prev_lines = _guard_reg_lines("prev", "prev_still")

    assert plate_lines, (
        'plate guard 안에 location_plate 등록이 없다 — _attach_plate 본문의 '
        '등록은 승자 확정 뒤에야 불리므로 이 갈래를 못 덮는다')
    assert prev_lines, 'prev guard 안에 prev_still 등록이 없다'

    assert min(plate_lines) < first_build, (
        f"plate guard 등록({min(plate_lines)}줄)이 "
        f"build_ab_branch_refs({first_build}줄) 뒤다")
    assert min(prev_lines) < first_build, (
        f"prev guard 등록({min(prev_lines)}줄)이 "
        f"build_ab_branch_refs({first_build}줄) 뒤다")


def test_plate_aid_resolution_is_shared():
    """★plate 자산 id 해석은 한 함수여야 한다 — 두 벌이면 갈린다."""
    import re
    from pathlib import Path

    src = (Path(__file__).resolve().parents[2] / "app" / "services"
           / "still_recipe_service.py").read_text(encoding="utf-8")
    assert "_resolve_plate_aid" in src, "plate aid helper 가 없다"
    m = re.search(r"def _attach_plate\(\).*?(?=\n            def |\n            if )",
                  src, re.S)
    assert m and "_resolve_plate_aid" in m.group(0), \
        "_attach_plate 가 자기 계산을 따로 한다 — 두 값이 갈린다"


def test_prev_still_is_registered_in_both_places():
    """★prev_still 은 자리가 둘이다 — non-bgfirst 와 bgfirst."""
    import re
    from pathlib import Path

    src = (Path(__file__).resolve().parents[2] / "app" / "services"
           / "still_recipe_service.py").read_text(encoding="utf-8")
    sites = re.findall(r'_register_ref\([^)]*"prev_still"', src)
    assert len(sites) >= 2, \
        f"prev_still 등록이 {len(sites)}곳 — bgfirst 갈래가 빠졌다"


def test_lane_seed_has_no_asset_lineage_and_we_admit_it():
    """★`lane_seed` 는 `_attach` 가 아예 없다 — 자산 계보가 어디에도 없다.

    **없는 것을 지어내지 않는다** — asset_id=None 으로 두고, 그 사실이
    조용히 잊히지 않게 여기서 못박는다.
    """
    import re
    from pathlib import Path

    src = (Path(__file__).resolve().parents[2] / "app" / "services"
           / "still_recipe_service.py").read_text(encoding="utf-8")
    assert not re.search(r'_attach\(\s*\n?\s*"lane_seed"', src), \
        "lane_seed 가 이제 _attach 된다 — 등록 목록에 보태고 이 시험을 지울 것"


def test_ambiguous_source_is_not_linked():
    """같은 바이트가 서로 다른 자산으로 등록되면 잇지 않는다."""
    _MISSING = object()
    m = {}

    def _reg(src, aid):
        k = ref_source_key(src)
        prev = m.get(k, _MISSING)
        if prev is _MISSING:
            m[k] = aid
        elif prev != aid:
            m[k] = None

    _reg(b"same", "asset-1")
    assert m[ref_source_key(b"same")] == "asset-1"
    _reg(b"same", "asset-2")
    assert m[ref_source_key(b"same")] is None      # 모호 → 안 잇는다
    _reg(b"same", "asset-1")
    assert m[ref_source_key(b"same")] is None      # 한 번 모호면 계속 모호


def test_every_branch_gets_ref_metadata():
    """_run_branch 를 지나는 모든 갈래가 신원을 받는다."""
    import ast
    from pathlib import Path

    src = (Path(__file__).resolve().parents[2] / "app" / "services"
           / "still_recipe_service.py").read_text(encoding="utf-8")
    tree = ast.parse(src)

    inner = [n for n in ast.walk(tree)
             if isinstance(n, ast.FunctionDef) and n.name == "_run_branch"]
    assert len(inner) == 1, "_run_branch 가 하나여야 한다"
    body = ast.dump(inner[0])
    assert "ref_role_metadata" in body, \
        "_run_branch 안에서 ref_role_metadata 를 안 만든다"
    assert "roll_ref_metadata" in body, \
        "_run_branch 안에서 roll_ref_metadata 를 안 만든다"


def test_source_key_is_stable_for_same_bytes():
    a, b = b"same-image-bytes", b"same-image-bytes"
    assert ref_source_key(a) == ref_source_key(b)
    assert ref_source_key(b"other") != ref_source_key(a)


def test_source_key_handles_paths():
    from pathlib import Path
    assert ref_source_key(Path("/a/b.png")) == ref_source_key("/a/b.png")


def test_join_survives_label_reformatting():
    """★라벨이 두 곳에서 다르다는 것이 이 접합의 존재 이유다."""
    src = b"char-image-bytes"
    asset_by_src = {ref_source_key(src): "char-asset-1"}

    labeled = build_still_refs(
        bg_only=False, plate=None, conti=None, prev_sel=None,
        char_refs=[("김선영", src)], prop_refs=[],
    )
    assert any("CHARACTER REFERENCE" in lab for lab, _ in labeled)
    assert not any(lab == "김선영" for lab, _ in labeled)

    hits = [asset_by_src.get(ref_source_key(s)) for _, s in labeled]
    assert "char-asset-1" in hits
