"""W-G (2026-07-03) — building_fp_link 결정론 테스트.

같은 building_groups 그룹의 indoor floor plan 을 outdoor 산출물의 구조 참조로
연결하는 순수 조인 로직: mixed 그룹 게이트 / anchor_loc 우선 / shot_count·loc_id
정렬 / fp 정렬 선택 / 소스 누락 방어. 전부 구조 필드 — 이름/라벨 파싱 없음.
"""
from app.modules.pipeline.building_fp_link import (
    build_group_membership_by_loc,
    build_outdoor_building_fp_link,
    indoor_fp_ids_by_loc,
)


def _member(loc, indoor, shots=1):
    return {"loc_id": loc, "is_indoor": indoor, "shot_count": shots}


# ── indoor_fp_ids_by_loc — catalog 구조 조인 ──


class TestIndoorFpIdsByLoc:
    def test_joins_loc_id_to_depends_on_fp(self):
        catalog = {
            "LAB01": {"loc_id": "LA", "depends_on_fp": ["fp_a_main"]},
            "LAB02": {"loc_id": "LA", "depends_on_fp": ["fp_a_main"]},
            "LBB01": {"loc_id": "LB", "depends_on_fp": ["fp_b_main", "fp_b_side"]},
            "LCB01": {"loc_id": "LC", "depends_on_fp": []},
        }
        out = indoor_fp_ids_by_loc(catalog)
        assert out == {
            "LA": ["fp_a_main"],
            "LB": ["fp_b_main", "fp_b_side"],
        }

    def test_sorted_and_deduped(self):
        catalog = {
            "X1": {"loc_id": "LA", "depends_on_fp": ["fp_z"]},
            "X2": {"loc_id": "LA", "depends_on_fp": ["fp_a", "fp_z"]},
        }
        assert indoor_fp_ids_by_loc(catalog) == {"LA": ["fp_a", "fp_z"]}

    def test_malformed_entries_ignored(self):
        catalog = {
            "X1": "not-a-dict",
            "X2": {"loc_id": None, "depends_on_fp": ["fp_a"]},
            "X3": {"loc_id": "LA", "depends_on_fp": [None, "", "fp_a"]},
        }
        assert indoor_fp_ids_by_loc(catalog) == {"LA": ["fp_a"]}

    def test_empty_catalog(self):
        assert indoor_fp_ids_by_loc({}) == {}
        assert indoor_fp_ids_by_loc(None) == {}


# ── build_outdoor_building_fp_link — 링크 조인/선택 규칙 ──


class TestBuildOutdoorBuildingFpLink:
    def test_mixed_group_links_all_outdoor_members(self):
        groups = [{
            "group_id": "g1", "anchor_loc": "L2",
            "members": [
                _member("L1", False, 6),
                _member("L2", True, 15),
                _member("L3", False, 2),
            ],
        }]
        link = build_outdoor_building_fp_link(groups, {"L2": ["fp_l2_main"]})
        assert link == {
            "L1": {"fp_id": "fp_l2_main", "indoor_loc_sid": "L2",
                   "group_id": "g1"},
            "L3": {"fp_id": "fp_l2_main", "indoor_loc_sid": "L2",
                   "group_id": "g1"},
        }

    def test_pure_outdoor_group_no_link(self):
        groups = [{
            "group_id": "g1", "anchor_loc": "L1",
            "members": [_member("L1", False, 3)],
        }]
        assert build_outdoor_building_fp_link(groups, {"L1": ["fp_x"]}) == {}

    def test_pure_indoor_group_no_link(self):
        groups = [{
            "group_id": "g1", "anchor_loc": "L1",
            "members": [_member("L1", True, 3)],
        }]
        assert build_outdoor_building_fp_link(groups, {"L1": ["fp_x"]}) == {}

    def test_anchor_loc_wins_over_shot_count(self):
        # anchor 가 indoor 후보면 shot_count 가 더 큰 다른 indoor 보다 우선.
        groups = [{
            "group_id": "g1", "anchor_loc": "L2",
            "members": [
                _member("L1", False, 2),
                _member("L2", True, 3),
                _member("L5", True, 9),
            ],
        }]
        link = build_outdoor_building_fp_link(
            groups, {"L2": ["fp_l2"], "L5": ["fp_l5"]})
        assert link["L1"]["fp_id"] == "fp_l2"
        assert link["L1"]["indoor_loc_sid"] == "L2"

    def test_anchor_without_fp_falls_back_to_shot_count(self):
        # anchor indoor 에 fp 가 없으면 fp 보유 indoor 중 shot_count 최대.
        groups = [{
            "group_id": "g1", "anchor_loc": "L2",
            "members": [
                _member("L1", False, 2),
                _member("L2", True, 9),
                _member("L5", True, 3),
                _member("L6", True, 3),
            ],
        }]
        link = build_outdoor_building_fp_link(
            groups, {"L5": ["fp_l5"], "L6": ["fp_l6"]})
        # shot_count 동률(3) → loc_id 오름차순 → L5.
        assert link["L1"]["indoor_loc_sid"] == "L5"
        assert link["L1"]["fp_id"] == "fp_l5"

    def test_indoor_without_any_fp_no_link(self):
        groups = [{
            "group_id": "g1", "anchor_loc": "L2",
            "members": [_member("L1", False, 2), _member("L2", True, 9)],
        }]
        assert build_outdoor_building_fp_link(groups, {}) == {}

    def test_multi_fp_indoor_picks_sorted_first(self):
        groups = [{
            "group_id": "g1", "anchor_loc": "L2",
            "members": [_member("L1", False, 2), _member("L2", True, 9)],
        }]
        link = build_outdoor_building_fp_link(
            groups, {"L2": ["fp_b", "fp_a"]})
        # indoor_fp_ids_by_loc 가 정렬을 보장하지만 직접 입력도 방어 —
        # build 는 리스트 첫 값을 취한다(호출 계약: 정렬 리스트).
        assert link["L1"]["fp_id"] == "fp_b"

    def test_duplicate_outdoor_loc_first_group_wins(self):
        groups = [
            {"group_id": "g1", "anchor_loc": "L2",
             "members": [_member("L1", False, 2), _member("L2", True, 9)]},
            {"group_id": "g2", "anchor_loc": "L5",
             "members": [_member("L1", False, 1), _member("L5", True, 4)]},
        ]
        link = build_outdoor_building_fp_link(
            groups, {"L2": ["fp_l2"], "L5": ["fp_l5"]})
        assert link["L1"]["group_id"] == "g1"
        assert link["L1"]["fp_id"] == "fp_l2"

    def test_malformed_groups_ignored(self):
        groups = [
            "not-a-dict",
            {"group_id": "g1", "members": "nope"},
            {"group_id": "g2", "anchor_loc": "L2",
             "members": [
                 "junk",
                 _member("L1", False, 2),
                 _member("L2", True, 9),
             ]},
        ]
        link = build_outdoor_building_fp_link(groups, {"L2": ["fp_l2"]})
        assert link == {
            "L1": {"fp_id": "fp_l2", "indoor_loc_sid": "L2",
                   "group_id": "g2"},
        }

    def test_empty_inputs(self):
        assert build_outdoor_building_fp_link([], {}) == {}
        assert build_outdoor_building_fp_link(None, {}) == {}

    def test_is_indoor_none_not_treated_as_outdoor(self):
        # is_indoor 미상(None)은 indoor 도 outdoor 도 아님 — 게이트/링크 제외.
        groups = [{
            "group_id": "g1", "anchor_loc": "L2",
            "members": [
                {"loc_id": "L1", "is_indoor": None, "shot_count": 2},
                _member("L2", True, 9),
            ],
        }]
        assert build_outdoor_building_fp_link(groups, {"L2": ["fp_l2"]}) == {}


# ── 실측 미러 (E2E 데이터 형태 — 시나리오 토큰 없음, 구조만) ──


class TestRealShapeMirror:
    def test_three_member_mixed_group_shape(self):
        """실측 building_groups 형태(외부/내부/외부 3멤버 + anchor=내부) 미러."""
        groups = [{
            "group_id": "bg_unit_a",
            "anchor_loc": "L04",
            "kind": "chain_bg",
            "members": [
                {"loc_id": "L03", "label": "x", "shot_count": 6,
                 "is_indoor": False, "summary": "y"},
                {"loc_id": "L04", "label": "x", "shot_count": 15,
                 "is_indoor": True, "summary": "y"},
                {"loc_id": "L10", "label": "x", "shot_count": 2,
                 "is_indoor": False, "summary": "y"},
            ],
        }]
        catalog = {
            "L03B01": {"loc_id": "L03", "depends_on_fp": ["fp_l03_main"]},
            "L04B01": {"loc_id": "L04", "depends_on_fp": ["fp_l04_room"]},
            "L10B01": {"loc_id": "L10", "depends_on_fp": ["fp_l10_a"]},
            "L10B02": {"loc_id": "L10", "depends_on_fp": ["fp_l10_b"]},
        }
        link = build_outdoor_building_fp_link(
            groups, indoor_fp_ids_by_loc(catalog))
        # 두 outdoor loc 전부 indoor 대표 fp 로 연결.
        assert set(link) == {"L03", "L10"}
        for loc in ("L03", "L10"):
            assert link[loc]["fp_id"] == "fp_l04_room"
            assert link[loc]["indoor_loc_sid"] == "L04"
            assert link[loc]["group_id"] == "bg_unit_a"


# ── W-K: build_group_membership_by_loc — 전 멤버 멤버십 조인 ──


class TestGroupMembershipByLoc:
    def test_all_members_indoor_and_outdoor(self):
        groups = [{
            "group_id": "g1",
            "anchor_loc": "L2",
            "members": [
                _member("L1", False, 6),
                _member("L2", True, 15),
                _member("L3", False, 2),
            ],
        }]
        out = build_group_membership_by_loc(groups)
        assert set(out) == {"L1", "L2", "L3"}
        assert out["L1"] == {
            "group_id": "g1", "is_indoor": False, "group_mixed": True}
        assert out["L2"] == {
            "group_id": "g1", "is_indoor": True, "group_mixed": True}

    def test_pure_indoor_group_not_mixed(self):
        groups = [{
            "group_id": "g_in",
            "members": [_member("L1", True), _member("L2", True)],
        }]
        out = build_group_membership_by_loc(groups)
        assert out["L1"]["group_mixed"] is False
        assert out["L2"]["is_indoor"] is True

    def test_non_bool_is_indoor_member_excluded(self):
        groups = [{
            "group_id": "g1",
            "members": [
                {"loc_id": "L1", "is_indoor": None, "shot_count": 2},
                _member("L2", False, 9),
            ],
        }]
        out = build_group_membership_by_loc(groups)
        assert set(out) == {"L2"}
        # 판정 가능한 멤버만으로 mixed 계산 — outdoor 단독 = not mixed.
        assert out["L2"]["group_mixed"] is False

    def test_duplicate_loc_first_group_wins(self):
        groups = [
            {"group_id": "g1", "members": [_member("L1", True)]},
            {"group_id": "g2",
             "members": [_member("L1", False), _member("L9", False)]},
        ]
        out = build_group_membership_by_loc(groups)
        assert out["L1"] == {
            "group_id": "g1", "is_indoor": True, "group_mixed": False}
        assert out["L9"]["group_id"] == "g2"

    def test_missing_group_id_or_empty_members_skipped(self):
        groups = [
            {"group_id": "", "members": [_member("L1", True)]},
            {"group_id": "g2", "members": []},
            "not-a-dict",
        ]
        assert build_group_membership_by_loc(groups) == {}
        assert build_group_membership_by_loc([]) == {}
