"""
닌기르수(Ningirsu) - dev5팀 테스터
task: organization-structure.json 멤버 ID와 parse-member-id.sh 매핑 완전성 검증

검증 항목:
- org-structure의 모든 멤버가 parse-member-id.sh에 매핑되어 있는지
- parse_member_id() bash 함수의 기본 동작 (한국어/영어 이름 → ID)
- substring 충돌 방지 (긴 패턴이 짧은 패턴보다 우선 매칭되어야 함)
- 관계없는 텍스트에서 false positive 없음
"""

import json
import re
import subprocess
from pathlib import Path

import pytest

ORG_STRUCTURE_PATH = Path("/home/jay/workspace/memory/organization-structure.json")
PARSE_MEMBER_ID_PATH = Path("/home/jay/.claude/hooks/lib/parse-member-id.sh")

# ── 헬퍼: org-structure에서 ID 수집 ─────────────────────────────────────────


def _extract_ids_from_node(node: object, collected: set[str]) -> None:
    """dict 노드에서 'id' 필드를 재귀적으로 수집"""
    if not isinstance(node, dict):
        return
    raw_id = node.get("id")
    if raw_id and isinstance(raw_id, str):
        # 제외 조건
        if raw_id not in ("consulting-lead", "venus-delegate"):
            collected.add(raw_id)


def _collect_team_ids(team: dict, collected: set[str]) -> None:
    """팀(또는 센터) 한 개에서 모든 멤버 ID를 수집"""
    # lead / office_lead / direct_worker
    for key in ("lead", "office_lead", "direct_worker"):
        node = team.get(key)
        if isinstance(node, dict):
            _extract_ids_from_node(node, collected)

    # members
    for member in team.get("members", []):
        _extract_ids_from_node(member, collected)

    # sub_teams (재귀)
    for sub in team.get("sub_teams", []):
        _collect_team_ids(sub, collected)


def get_org_member_ids() -> set[str]:
    """organization-structure.json에서 유효한 에이전트 ID 전체를 반환"""
    with ORG_STRUCTURE_PATH.open(encoding="utf-8") as f:
        org = json.load(f)

    structure = org.get("structure", {})
    collected: set[str] = set()

    # columns.teams
    for team in structure.get("columns", {}).get("teams", []):
        _collect_team_ids(team, collected)

    # rows.centers
    for center in structure.get("rows", {}).get("centers", []):
        _collect_team_ids(center, collected)

    return collected


def get_mapped_ids() -> set[str]:
    """parse-member-id.sh 또는 동적 캐시에서 매핑된 ID를 반환"""
    ids: set[str] = set()
    # 직접 하드코딩 또는 동적 캐시 파일 모두 지원
    for path in [
        PARSE_MEMBER_ID_PATH,
        PARSE_MEMBER_ID_PATH.parent / ".member-map-cache.sh",
    ]:
        if path.exists():
            content = path.read_text(encoding="utf-8")
            ids.update(re.findall(r'echo "(\w+)"', content))
    ids.discard("")
    return ids


# ── 테스트 ──────────────────────────────────────────────────────────────────


def test_all_org_members_mapped():
    """organization-structure.json의 모든 멤버가 parse-member-id.sh에 매핑되어 있는지 검증"""
    if not ORG_STRUCTURE_PATH.exists():
        pytest.skip(f"org-structure 파일 없음: {ORG_STRUCTURE_PATH}")
    if not PARSE_MEMBER_ID_PATH.exists():
        pytest.skip(f"parse-member-id.sh 파일 없음: {PARSE_MEMBER_ID_PATH}")

    org_ids = get_org_member_ids()
    mapped_ids = get_mapped_ids()

    missing = org_ids - mapped_ids

    if missing:
        # 누락 목록을 알파벳 순으로 출력해 디버깅 편의 제공
        missing_sorted = sorted(missing)
        print("\n[MISSING] org-structure에 있지만 parse-member-id.sh에 없는 ID:")
        for m_id in missing_sorted:
            print(f"  - {m_id}")
        pytest.fail(
            f"총 {len(missing)}개 에이전트가 매핑 누락:\n  "
            + "\n  ".join(missing_sorted)
        )


def _call_parse_member_id(text: str) -> str:
    """bash에서 parse_member_id 함수를 호출하고 결과 반환"""
    script = str(PARSE_MEMBER_ID_PATH)
    result = subprocess.run(
        ["bash", "-c", f'source "{script}" && parse_member_id "$1"', "_", text],
        capture_output=True,
        text=True,
        timeout=5,
    )
    return result.stdout.strip()


def test_parse_member_id_basic():
    """parse_member_id() 기본 동작 검증 - subprocess로 bash 호출"""
    if not PARSE_MEMBER_ID_PATH.exists():
        pytest.skip(f"parse-member-id.sh 파일 없음: {PARSE_MEMBER_ID_PATH}")

    # (입력 텍스트, 기대 ID) 쌍
    cases = [
        # 한국어 이름
        ("헤르메스가 작업을 시작했습니다", "hermes"),
        ("오딘 팀장님 확인 부탁드립니다", "odin"),
        ("마르둑 보고", "marduk"),
        # 영어 이름
        ("Hermes has started the task", "hermes"),
        ("Report from Odin", "odin"),
        ("Marduk completed", "marduk"),
    ]

    for text, expected_id in cases:
        actual = _call_parse_member_id(text)
        assert (
            actual == expected_id
        ), f"입력 '{text}' → 기대 '{expected_id}', 실제 '{actual}'"


def test_parse_member_id_substring_collision():
    """substring 충돌 테스트 - '아누비스'가 '아누'보다 우선 매칭"""
    if not PARSE_MEMBER_ID_PATH.exists():
        pytest.skip(f"parse-member-id.sh 파일 없음: {PARSE_MEMBER_ID_PATH}")

    # '아누비스'는 '아누'를 포함하므로, 긴 패턴이 우선 매칭되어야 한다
    collision_cases = [
        ("아누비스가 데이터를 처리했습니다", "anubis"),  # 'anubis', not 'anu'
        ("아누 실장님 지시입니다", "anu"),  # 'anu' (단독)
    ]

    for text, expected_id in collision_cases:
        actual = _call_parse_member_id(text)
        assert (
            actual == expected_id
        ), f"[substring 충돌] 입력 '{text}' → 기대 '{expected_id}', 실제 '{actual}'"


def test_no_false_positive():
    """관계없는 텍스트에서 false positive 없음"""
    if not PARSE_MEMBER_ID_PATH.exists():
        pytest.skip(f"parse-member-id.sh 파일 없음: {PARSE_MEMBER_ID_PATH}")

    unrelated_texts = [
        "오늘 날씨가 맑습니다",
        "배포 완료 알림",
        "테스트 결과 보고서",
        "unknown random text with no agent names",
    ]

    for text in unrelated_texts:
        actual = _call_parse_member_id(text)
        assert (
            actual == ""
        ), f"[false positive] 무관 텍스트 '{text}' → 예상 '' (빈 문자열), 실제 '{actual}'"
