"""축 A1 파일럿 — entity_t2i user 프롬프트 조립 재배열.

계약 두 가지를 시험한다.
  ① OFF(기본): 기존 조립식 `render(**fields) + project + call` 과 글자 하나까지 같다.
  ② ON: 조각(문단)의 다중집합이 OFF 와 같고(내용 무손실), 정적 접두부가
     대상 엔티티가 달라져도 동일하다.

②의 접두부는 두 엔티티 fixture 로 잰다 — 재배열의 목적이 "호출이 달라져도
앞쪽이 같게 두는 것"이므로, 같은 엔티티 하나로는 증명이 되지 않는다.
"""
import pytest

from app.core.config import settings
from app.core.prompt_assembly import (
    PARA_SEP,
    assemble_user_prompt,
    paragraph_multiset,
    reorder_enabled,
    split_variable_paragraphs,
)
from app.modules.prompt_loader import load_prompt

STEP = "entity_t2i"

# 프로젝트 단위로 고정인 블록 (실제 조립과 같은 모양 — "\n\n" 로 시작).
PROJECT_BLOCK = (
    "\n\n[세계관]\n시대: 〈작품 시대〉\n지역: 〈작품 지역〉"
    "\n의상규칙: 〈의상 규칙〉"
    "\n\n[T2I 시각 컨텍스트]\n〈시각 컨텍스트 문단〉"
)

# 두 엔티티 fixture — 이름·타입·상세가 모두 다르다.
ENTITY_A = {
    "fields": {"entity_name": "우산", "entity_type": "prop"},
    "call_block": (
        "\n\n[시나리오 기반 상세 정보]\n"
        "설명: 폭이 넓은 장우산.\n시각적 특징: 긴 우산대, 어두운 천"
    ),
}
ENTITY_B = {
    "fields": {"entity_name": "언덕 위 공터", "entity_type": "location"},
    "call_block": (
        "\n\n[시나리오 기반 상세 정보]\n"
        "설명: 나무 몇 그루가 선 빈 터.\n시각적 특징: 마른 풀, 낮은 담"
    ),
}


def _render(**kwargs) -> str:
    """실제 스텝과 같은 경로로 팩 템플릿을 렌더한다."""
    return load_prompt("entity_extractor_v2", "turn_entity_detail", **kwargs)


def _legacy_assemble(entity) -> str:
    """재배열 도입 전의 조립식."""
    return _render(**entity["fields"]) + PROJECT_BLOCK + entity["call_block"]


def _assemble(entity) -> str:
    return assemble_user_prompt(
        STEP,
        render=_render,
        fields=entity["fields"],
        project_block=PROJECT_BLOCK,
        call_block=entity["call_block"],
    )


@pytest.fixture
def reorder_on(monkeypatch):
    monkeypatch.setattr(settings, "prompt_assembly_reorder_steps", STEP)


@pytest.fixture
def reorder_off(monkeypatch):
    monkeypatch.setattr(settings, "prompt_assembly_reorder_steps", "")


# ── ① OFF = 기존 조립과 byte-identical ──


@pytest.mark.parametrize("entity", [ENTITY_A, ENTITY_B])
def test_off_is_byte_identical_to_legacy(reorder_off, entity):
    assert _assemble(entity) == _legacy_assemble(entity)


def test_off_is_default_without_env(monkeypatch):
    """플래그를 건드리지 않은 상태(기본값)에서도 재배열은 꺼져 있다."""
    monkeypatch.delenv("PROMPT_ASSEMBLY_REORDER_STEPS", raising=False)
    assert type(settings).model_fields["prompt_assembly_reorder_steps"].default == ""


@pytest.mark.parametrize(
    "flag,expected",
    [
        ("", False),
        ("entity_t2i", True),
        (" entity_t2i , scene_detail ", True),
        ("scene_detail", False),
        ("entity_t2i_extra", False),
    ],
)
def test_reorder_enabled_parses_step_list(monkeypatch, flag, expected):
    monkeypatch.setattr(settings, "prompt_assembly_reorder_steps", flag)
    assert reorder_enabled(STEP) is expected


# ── ② ON = 내용 무손실 + 정적 접두부 고정 ──


@pytest.mark.parametrize("entity", [ENTITY_A, ENTITY_B])
def test_on_preserves_all_paragraphs(reorder_on, entity):
    """조각의 다중집합이 같다 — 지운 것도 더한 것도 없다."""
    on = _assemble(entity)
    off = _legacy_assemble(entity)
    assert paragraph_multiset(on) == paragraph_multiset(off)
    assert len(on) == len(off)


@pytest.mark.parametrize("entity", [ENTITY_A, ENTITY_B])
def test_on_moves_variable_head_to_the_tail(reorder_on, entity):
    """호출마다 변하는 머리 문단이 뒤로 간다."""
    static, variable = split_variable_paragraphs(_render, entity["fields"])
    assert len(variable) == 1, "팩 템플릿의 가변 문단은 머리 한 개여야 한다"
    head = variable[0]
    assert entity["fields"]["entity_name"] in head

    on = _assemble(entity)
    assert not on.startswith(head)
    # 머리 문단은 프로젝트 블록 뒤, 호출 상세 앞에 온다.
    assert on.index(head) > on.index("[T2I 시각 컨텍스트]")
    assert on.index(head) < on.index("[시나리오 기반 상세 정보]")


def test_on_static_prefix_is_identical_across_entities(reorder_on):
    """엔티티가 달라도 앞쪽이 같다 — 재배열의 목적 그 자체."""
    a, b = _assemble(ENTITY_A), _assemble(ENTITY_B)
    prefix = _common_prefix(a, b)

    static, _variable = split_variable_paragraphs(_render, ENTITY_A["fields"])
    static_text = PARA_SEP.join(static)

    # 정적 지시 전문 + 프로젝트 블록까지가 통째로 공통 접두부다.
    assert prefix.startswith(static_text)
    assert (static_text + PROJECT_BLOCK) == prefix[: len(static_text + PROJECT_BLOCK)]
    # 스키마 계약 문단이 접두부 안에 살아 있다(우연한 짧은 일치 차단).
    assert "D6 + Area B" in prefix


def test_on_static_prefix_is_longer_than_off(reorder_off, monkeypatch):
    """수치로도 확인 — OFF 는 첫 줄에서 갈리고 ON 은 정적 전문이 남는다."""
    off_prefix = len(_common_prefix(_assemble(ENTITY_A), _assemble(ENTITY_B)))
    monkeypatch.setattr(settings, "prompt_assembly_reorder_steps", STEP)
    on_prefix = len(_common_prefix(_assemble(ENTITY_A), _assemble(ENTITY_B)))
    print(f"\n공통 접두부(자): OFF={off_prefix} ON={on_prefix}")
    assert on_prefix > off_prefix
    assert off_prefix < 10  # "요소 \"" 까지에서 갈린다


# ── 안전장치 ──


def test_paragraph_separator_inside_value_raises():
    """값에 문단 구분자가 들어오면 조용히 넘기지 않고 멈춘다."""
    with pytest.raises(ValueError):
        split_variable_paragraphs(
            _render,
            {"entity_name": "이름\n\n두 문단", "entity_type": "prop"},
        )


def _common_prefix(a: str, b: str) -> str:
    i = 0
    while i < min(len(a), len(b)) and a[i] == b[i]:
        i += 1
    return a[:i]
