"""G3.1 _evidence_helpers unit tests."""
import logging
import pytest

from app.core.errors import AppError
from app.core.steps._evidence_helpers import (
    LEGACY_CONFIDENCE,
    EVIDENCE_FIELDS,
    VALID_LLM_CONFIDENCE,
    VALID_STORED_CONFIDENCE,
    _normalize_evidence_fields,
    _normalize_scene_consistency_result,
    _normalize_scene_detail_result,
    assert_fresh_llm_evidence,
)


class TestConstants:
    def test_legacy_confidence_value(self):
        assert LEGACY_CONFIDENCE == "legacy"

    def test_evidence_fields_3(self):
        assert EVIDENCE_FIELDS == (
            "source_facts", "visual_inferences", "creative_decisions"
        )

    def test_valid_llm_confidence_excludes_legacy(self):
        assert "legacy" not in VALID_LLM_CONFIDENCE
        assert set(VALID_LLM_CONFIDENCE) == {"high", "medium", "low"}

    def test_valid_stored_includes_legacy(self):
        assert "legacy" in VALID_STORED_CONFIDENCE


class TestNormalizeEvidenceFields:
    def test_all_fields_present_high_confidence_noop(self):
        item = {
            "source_facts": ["fact 1"],
            "visual_inferences": ["inf 1"],
            "creative_decisions": ["dec 1"],
            "confidence": "high",
        }
        result = _normalize_evidence_fields(item)
        assert result is item  # in-place
        assert result["confidence"] == "high"
        assert result["source_facts"] == ["fact 1"]

    def test_all_fields_missing_becomes_legacy(self):
        item = {"description": "x"}
        _normalize_evidence_fields(item)
        assert item["source_facts"] == []
        assert item["visual_inferences"] == []
        assert item["creative_decisions"] == []
        assert item["confidence"] == LEGACY_CONFIDENCE

    def test_partial_only_source_facts_becomes_legacy(self):
        item = {"source_facts": ["fact"]}
        _normalize_evidence_fields(item)
        assert item["visual_inferences"] == []
        assert item["creative_decisions"] == []
        assert item["confidence"] == LEGACY_CONFIDENCE

    def test_partial_with_real_confidence_still_legacy(self):
        item = {"confidence": "high"}
        _normalize_evidence_fields(item)
        assert item["confidence"] == LEGACY_CONFIDENCE

    def test_invalid_confidence_string_becomes_legacy(self):
        item = {
            "source_facts": [],
            "visual_inferences": [],
            "creative_decisions": [],
            "confidence": "unknown",
        }
        _normalize_evidence_fields(item)
        assert item["confidence"] == LEGACY_CONFIDENCE

    def test_non_list_source_facts_replaced(self):
        item = {
            "source_facts": "not a list",
            "visual_inferences": [],
            "creative_decisions": [],
            "confidence": "high",
        }
        _normalize_evidence_fields(item)
        assert item["source_facts"] == []
        assert item["confidence"] == LEGACY_CONFIDENCE

    def test_none_source_facts_replaced(self):
        item = {
            "source_facts": None,
            "visual_inferences": [],
            "creative_decisions": [],
            "confidence": "medium",
        }
        _normalize_evidence_fields(item)
        assert item["source_facts"] == []

    def test_non_dict_input_returned_unchanged(self):
        for inp in [None, [], "string", 42]:
            assert _normalize_evidence_fields(inp) == inp

    def test_legacy_confidence_already_set_preserved(self):
        item = {
            "source_facts": [],
            "visual_inferences": [],
            "creative_decisions": [],
            "confidence": "legacy",
        }
        _normalize_evidence_fields(item)
        assert item["confidence"] == "legacy"

    def test_where_param_logs_marker(self, caplog):
        caplog.set_level(logging.DEBUG, logger="app.core.steps._evidence_helpers")
        item = {}
        _normalize_evidence_fields(item, where="test_caller")
        assert any(
            "marker=legacy" in r.message and "test_caller" in r.message
            for r in caplog.records
        )


class TestNormalizeSceneConsistency:
    def test_normalize_each_fixed_element(self):
        scene = {"scene_index": 1, "fixed_elements": [{}, {"confidence": "high"}]}
        _normalize_scene_consistency_result(scene)
        assert scene["fixed_elements"][0]["confidence"] == LEGACY_CONFIDENCE
        assert scene["fixed_elements"][1]["confidence"] == LEGACY_CONFIDENCE

    def test_empty_fixed_elements_noop(self):
        scene = {"scene_index": 1, "fixed_elements": []}
        result = _normalize_scene_consistency_result(scene)
        assert result["fixed_elements"] == []

    def test_missing_fixed_elements_key_coerced_empty_list(self):
        # Codex BLOCKING 2 fix: helper 가 'list 아니면 silent return' 으로 끝내면
        # downstream consumer 가 list 가정 → AttributeError. malformed 면 in-place
        # 빈 list 강제 → consumer 정상 (noop iter). missing 도 동일 패턴.
        scene = {"scene_index": 1, "status": "blocked_no_selection"}
        result = _normalize_scene_consistency_result(scene)
        assert result["fixed_elements"] == []

    def test_malformed_fixed_elements_dict_coerced_empty_list(self):
        # Codex BLOCKING 2 fix: in-place coerce. 옛 데이터 손실은 어차피 malformed.
        scene = {"scene_index": 1, "fixed_elements": {"not": "a list"}}
        result = _normalize_scene_consistency_result(scene)
        assert result["fixed_elements"] == []

    def test_non_dict_input_returned_unchanged(self):
        assert _normalize_scene_consistency_result(None) is None
        assert _normalize_scene_consistency_result([]) == []


class TestNormalizeSceneDetail:
    def test_normalize_each_t2i_variation(self):
        scene = {
            "scene_index": 1,
            "t2i_variations": [
                {"variant_label": "var_1"},
                {"variant_label": "var_2", "confidence": "high"},
            ],
        }
        _normalize_scene_detail_result(scene)
        assert scene["t2i_variations"][0]["confidence"] == LEGACY_CONFIDENCE
        assert scene["t2i_variations"][1]["confidence"] == LEGACY_CONFIDENCE

    def test_empty_t2i_variations_noop(self):
        scene = {"scene_index": 1, "t2i_variations": []}
        result = _normalize_scene_detail_result(scene)
        assert result["t2i_variations"] == []

    def test_missing_t2i_variations_key_coerced_empty_list(self):
        # Codex BLOCKING 2 fix: malformed/missing in-place coerce — consumer 정상.
        scene = {"scene_index": 1, "scene_type": "normal"}
        result = _normalize_scene_detail_result(scene)
        assert result["t2i_variations"] == []

    def test_malformed_t2i_variations_string_coerced_empty_list(self):
        # Codex BLOCKING 2 fix: in-place coerce. malformed string 데이터 손실 OK.
        scene = {"scene_index": 1, "t2i_variations": "not a list"}
        result = _normalize_scene_detail_result(scene)
        assert result["t2i_variations"] == []

    def test_non_dict_input_returned_unchanged(self):
        assert _normalize_scene_detail_result(None) is None


class TestAssertFreshLLMEvidence:
    def test_high_confidence_with_facts_passes(self):
        item = {
            "source_facts": ["fact"],
            "visual_inferences": [],
            "creative_decisions": [],
            "confidence": "high",
        }
        assert_fresh_llm_evidence(item, "scene_consistency")

    def test_low_confidence_empty_facts_passes(self):
        item = {
            "source_facts": [],
            "visual_inferences": ["inf"],
            "creative_decisions": [],
            "confidence": "low",
        }
        assert_fresh_llm_evidence(item, "scene_consistency")

    def test_legacy_confidence_rejected(self):
        item = {
            "source_facts": ["x"],
            "visual_inferences": [],
            "creative_decisions": [],
            "confidence": "legacy",
        }
        with pytest.raises(AppError) as exc:
            assert_fresh_llm_evidence(item, "scene_consistency")
        assert exc.value.code == "step.contract_violation"
        assert "legacy" in exc.value.message

    def test_empty_source_facts_high_confidence_rejected(self):
        item = {
            "source_facts": [],
            "visual_inferences": ["inf"],
            "creative_decisions": [],
            "confidence": "high",
        }
        with pytest.raises(AppError) as exc:
            assert_fresh_llm_evidence(item, "scene_consistency")
        assert exc.value.code == "step.contract_violation"
        assert "source_facts" in exc.value.message

    def test_empty_source_facts_medium_confidence_rejected(self):
        item = {
            "source_facts": [],
            "visual_inferences": ["inf"],
            "creative_decisions": [],
            "confidence": "medium",
        }
        with pytest.raises(AppError):
            assert_fresh_llm_evidence(item, "scene_detail")

    def test_all_lists_empty_high_confidence_rejected(self):
        item = {
            "source_facts": [],
            "visual_inferences": [],
            "creative_decisions": [],
            "confidence": "high",
        }
        with pytest.raises(AppError) as exc:
            assert_fresh_llm_evidence(item, "scene_detail")
        assert exc.value.code == "step.contract_violation"

    def test_all_lists_empty_low_passes(self):
        item = {
            "source_facts": [],
            "visual_inferences": [],
            "creative_decisions": [],
            "confidence": "low",
        }
        assert_fresh_llm_evidence(item, "scene_consistency")

    def test_step_name_in_error_message(self):
        item = {
            "source_facts": [],
            "visual_inferences": [],
            "creative_decisions": [],
            "confidence": "high",
        }
        with pytest.raises(AppError) as exc:
            assert_fresh_llm_evidence(item, "MY_STEP_NAME")
        assert "MY_STEP_NAME" in exc.value.message
