"""StepRunner editorial cascade 정책 런타임 검증.

editorial step이 타 체크포인트를 수정한 뒤 invalidate_downstream을 호출하는지
manifest flag `invalidate_downstream_on_edit`에 따라 분기되는지 단위 테스트로 고정.

정책: 기본(flag 미설정 또는 False)이면 cascade 안 함. True 명시하면 cascade 발동.
"""
from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from app.core.step_runner import StepRunner


class _FakeEditorialStep(StepRunner):
    """테스트용 editorial StepRunner — _execute는 mock 결과 반환."""

    def __init__(self, manifest: dict):
        # StepRunner.__init__는 DB 등 많은 설정을 요구하므로 __new__로 우회
        pass

    def _execute(self, mode="resume"):
        return {
            "completed_count": 1,
            "applicable_count": 1,
            "failed_count": 0,
            "data": {},
        }


def _make_instance(manifest: dict) -> _FakeEditorialStep:
    inst = _FakeEditorialStep.__new__(_FakeEditorialStep)
    inst.step_id = "fake_editorial"
    inst.project_id = "p1"
    inst.episode_id = "e1"
    inst.db = MagicMock()
    inst.project_config = {}
    inst.manifest = manifest
    inst.run_id = "r1"
    inst.opik_context = {}
    return inst


def _prep_mocks(inst: _FakeEditorialStep):
    """_run() 호출에 필요한 내부 메서드 mock."""
    inst._update_step_run = MagicMock()
    inst.save_checkpoint = MagicMock()
    inst.invalidate_downstream = MagicMock()
    inst.build_opik_metadata = MagicMock(return_value={})


def test_editorial_no_cascade_when_flag_absent():
    """manifest에 invalidate_downstream_on_edit 없으면 cascade 안 됨."""
    manifest = {
        "step_type": "editorial",
        "modifies_checkpoints": ["target_a", "target_b"],
        # invalidate_downstream_on_edit 필드 없음 → 기본 False
    }
    inst = _make_instance(manifest)
    _prep_mocks(inst)

    with patch("app.modules.llm.llm_client.set_opik_context"):
        inst.run(mode="resume")

    inst.invalidate_downstream.assert_not_called()


def test_editorial_no_cascade_when_flag_false():
    """flag=False 명시하면 cascade 안 됨."""
    manifest = {
        "step_type": "editorial",
        "modifies_checkpoints": ["target_a"],
        "invalidate_downstream_on_edit": False,
    }
    inst = _make_instance(manifest)
    _prep_mocks(inst)

    with patch("app.modules.llm.llm_client.set_opik_context"):
        inst.run(mode="resume")

    inst.invalidate_downstream.assert_not_called()


def test_editorial_cascade_when_flag_true():
    """flag=True 명시하면 modifies_checkpoints 타겟마다 cascade 발동."""
    manifest = {
        "step_type": "editorial",
        "modifies_checkpoints": ["target_a", "target_b"],
        "invalidate_downstream_on_edit": True,
    }
    inst = _make_instance(manifest)
    _prep_mocks(inst)

    with patch("app.modules.llm.llm_client.set_opik_context"):
        inst.run(mode="resume")

    assert inst.invalidate_downstream.call_count == 2
    call_kwargs = [c.kwargs for c in inst.invalidate_downstream.call_args_list]
    targets = {c["target_step_id"] for c in call_kwargs}
    assert targets == {"target_a", "target_b"}
    for c in call_kwargs:
        assert c["delete_checkpoints"] is False


def test_editorial_no_cascade_when_modifies_checkpoints_empty():
    """modifies_checkpoints가 비어 있으면 flag 상관없이 cascade 안 됨."""
    manifest = {
        "step_type": "editorial",
        "modifies_checkpoints": [],
        "invalidate_downstream_on_edit": True,
    }
    inst = _make_instance(manifest)
    _prep_mocks(inst)

    with patch("app.modules.llm.llm_client.set_opik_context"):
        inst.run(mode="resume")

    inst.invalidate_downstream.assert_not_called()
