"""
Additional coverage tests for parser_impl.py helper functions.
Targets uncovered lines: 61, 64, 71, 77, 79-80, 103, 105, 119-121, 125-127,
131-133, 145-152, 156-164, 168-175, 179-189, 200-218, 228-250, 256, 262,
270-307, 315-428, 432-494, 574-620, 636-640, ...
"""
from __future__ import annotations

from mjr_am_backend.features.geninfo import parser_impl as p

# ─── _clean_model_id edge cases ────────────────────────────────────────────

def test_clean_model_id_none():
    assert p._clean_model_id(None) is None


def test_clean_model_id_empty_string():
    assert p._clean_model_id("") is None
    assert p._clean_model_id("   ") is None


def test_clean_model_id_no_extension():
    assert p._clean_model_id("mymodel") == "mymodel"


def test_clean_model_id_backslash():
    assert p._clean_model_id("C:\\models\\foo.safetensors") == "foo"


# ─── _to_int edge cases ─────────────────────────────────────────────────────

def test_to_int_none():
    assert p._to_int(None) is None


def test_to_int_invalid():
    assert p._to_int("not_a_number") is None


def test_to_int_valid():
    assert p._to_int("42") == 42


# ─── _looks_like_prompt_string ─────────────────────────────────────────────

def test_looks_like_prompt_string_control_chars():
    # Has control char below \x09 → should return False
    assert p._looks_like_prompt_string("abcdefg\x01") is False


def test_looks_like_prompt_string_short():
    assert p._looks_like_prompt_string("ab") is False


def test_looks_like_prompt_string_non_string():
    assert p._looks_like_prompt_string(123) is False


# ─── _field family empty string paths ──────────────────────────────────────

def test_field_empty_string():
    assert p._field("", "high", "src") is None


def test_field_name_empty_string():
    assert p._field_name("", "high", "src") is None


def test_field_size_none_height():
    assert p._field_size(512, None, "high", "src") is None


# ─── _is_sampler ───────────────────────────────────────────────────────────

def test_is_sampler_no_class_type():
    assert p._is_sampler({}) is False


def test_is_sampler_ksampler():
    assert p._is_sampler({"class_type": "KSampler", "inputs": {"steps": 20, "cfg": 7, "seed": 1}}) is True


def test_is_sampler_core_signature():
    node = {"class_type": "CustomStep", "inputs": {"steps": 20, "cfg": 7.0, "seed": 42}}
    assert p._is_sampler(node) is True


def test_parse_geninfo_prefers_sink_with_real_sampler_branch():
    prompt_graph = {
        "10": {"class_type": "VHS_VideoCombine", "inputs": {"images": ["11", 0]}},
        "11": {"class_type": "CreateVideo", "inputs": {"images": ["12", 0]}},
        "12": {"class_type": "LoadImage", "inputs": {"image": "preview.png"}},
        "20": {"class_type": "SaveVideo", "inputs": {"video": ["21", 0]}},
        "21": {"class_type": "CreateVideo", "inputs": {"images": ["22", 0], "fps": ["30", 0]}},
        "22": {"class_type": "VAEDecode", "inputs": {"samples": ["23", 0], "vae": ["27", 0]}},
        "23": {
            "class_type": "SamplerCustomAdvanced",
            "inputs": {
                "noise": ["24", 0],
                "guider": ["25", 0],
                "sampler": ["26", 0],
                "sigmas": ["28", 0],
                "latent_image": ["29", 0],
            },
        },
        "24": {"class_type": "RandomNoise", "inputs": {"noise_seed": 42}},
        "25": {"class_type": "BasicGuider", "inputs": {"conditioning": ["31", 0], "model": ["32", 0]}},
        "26": {"class_type": "KSamplerSelect", "inputs": {"sampler_name": "euler"}},
        "27": {"class_type": "VAELoader", "inputs": {"vae_name": "ae.safetensors"}},
        "28": {"class_type": "BasicScheduler", "inputs": {"steps": 25, "scheduler": "simple", "model": ["32", 0]}},
        "29": {"class_type": "EmptyLatentImage", "inputs": {"width": 1024, "height": 576}},
        "30": {"class_type": "Float", "inputs": {"value": 24.0}},
        "31": {"class_type": "CLIPTextEncode", "inputs": {"text": "Blooming flower", "clip": ["33", 0]}},
        "32": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "ltx-2.3-22b-dev.safetensors"}},
        "33": {"class_type": "CLIPLoader", "inputs": {"clip_name": "clip.safetensors"}},
    }

    result = p.parse_geninfo_from_prompt(prompt_graph)

    assert result.ok, result.error
    assert result.data is not None
    assert result.data["positive"]["value"] == "Blooming flower"
    assert result.data["seed"]["value"] == 42
    assert result.data["steps"]["value"] == 25
    assert result.data["sampler"]["name"] == "euler"


def test_parse_geninfo_traces_final_generated_prompt_through_preview_switch():
    prompt_graph = {
        "1": {"class_type": "SaveImage", "inputs": {"images": ["2", 0]}},
        "2": {"class_type": "VAEDecode", "inputs": {"samples": ["3", 0], "vae": ["9", 0]}},
        "3": {
            "class_type": "KSampler",
            "inputs": {
                "seed": 11,
                "steps": 8,
                "cfg": 1.0,
                "sampler_name": "euler",
                "scheduler": "simple",
                "denoise": 1.0,
                "model": ["8", 0],
                "positive": ["4", 0],
                "negative": ["10", 0],
                "latent_image": ["11", 0],
            },
        },
        "4": {"class_type": "CLIPTextEncode", "inputs": {"text": ["5", 0], "clip": ["7", 0]}},
        "5": {
            "class_type": "ComfySwitchNode",
            "inputs": {"switch": ["12", 0], "on_false": ["6", 0], "on_true": ["13", 0]},
        },
        "6": {
            "class_type": "PreviewAny",
            "inputs": {
                "source": ["14", 0],
                "text_0": "final enhanced prompt with surreal glass and ink doodles",
            },
        },
        "7": {"class_type": "CLIPLoader", "inputs": {"clip_name": "clip.safetensors"}},
        "8": {"class_type": "UNETLoader", "inputs": {"unet_name": "model.safetensors"}},
        "9": {"class_type": "VAELoader", "inputs": {"vae_name": "vae.safetensors"}},
        "10": {"class_type": "ConditioningZeroOut", "inputs": {"conditioning": ["4", 0]}},
        "11": {"class_type": "EmptyLatentImage", "inputs": {"width": 1024, "height": 1024}},
        "12": {"class_type": "PrimitiveBoolean", "inputs": {"value": False}},
        "13": {
            "class_type": "StringConcatenate",
            "inputs": {"string_a": ["6", 0], "string_b": "trigger", "delimiter": ", "},
        },
        "14": {
            "class_type": "TextGenerate",
            "inputs": {"prompt": ["15", 0], "max_length": 512},
        },
        "15": {
            "class_type": "PrimitiveStringMultiline",
            "inputs": {"value": "original short user prompt"},
        },
    }

    result = p.parse_geninfo_from_prompt(prompt_graph)

    assert result.ok, result.error
    assert result.data is not None
    assert result.data["positive"]["value"] == "final enhanced prompt with surreal glass and ink doodles"
    assert result.data["positive"]["source"] == "CLIPTextEncode:4"


def test_parse_geninfo_generator_fallback_uses_user_input_not_system_template():
    prompt_graph = {
        "1": {"class_type": "SaveImage", "inputs": {"images": ["2", 0]}},
        "2": {"class_type": "VAEDecode", "inputs": {"samples": ["3", 0], "vae": ["9", 0]}},
        "3": {
            "class_type": "KSampler",
            "inputs": {
                "seed": 11,
                "steps": 8,
                "cfg": 1.0,
                "sampler_name": "euler",
                "scheduler": "simple",
                "model": ["8", 0],
                "positive": ["4", 0],
                "negative": ["10", 0],
                "latent_image": ["11", 0],
            },
        },
        "4": {"class_type": "CLIPTextEncode", "inputs": {"text": ["5", 0], "clip": ["7", 0]}},
        "5": {"class_type": "PreviewAny", "inputs": {"source": ["6", 0]}},
        "6": {"class_type": "TextGenerate", "inputs": {"prompt": ["12", 0], "max_length": 512}},
        "7": {"class_type": "CLIPLoader", "inputs": {"clip_name": "clip.safetensors"}},
        "8": {"class_type": "UNETLoader", "inputs": {"unet_name": "model.safetensors"}},
        "9": {"class_type": "VAELoader", "inputs": {"vae_name": "vae.safetensors"}},
        "10": {"class_type": "ConditioningZeroOut", "inputs": {"conditioning": ["4", 0]}},
        "11": {"class_type": "EmptyLatentImage", "inputs": {"width": 1024, "height": 1024}},
        "12": {
            "class_type": "StringConcatenate",
            "inputs": {
                "string_a": ["13", 0],
                "string_b": ["14", 0],
                "delimiter": "",
            },
        },
        "13": {
            "class_type": "PrimitiveStringMultiline",
            "inputs": {"value": "You are an expert prompt engineer.\n\nUser's Input:\n\n"},
        },
        "14": {
            "class_type": "PrimitiveStringMultiline",
            "inputs": {"value": "original detailed martini glass illustration prompt"},
        },
    }

    result = p.parse_geninfo_from_prompt(prompt_graph)

    assert result.ok, result.error
    assert result.data is not None
    assert result.data["positive"]["value"] == "original detailed martini glass illustration prompt"


# ─── _is_named_sampler_type ────────────────────────────────────────────────

def test_is_named_sampler_ksampler_select():
    # "ksampler" + "select" → False
    assert p._is_named_sampler_type("ksamplerselect") is False


def test_is_named_sampler_ksampler_only():
    assert p._is_named_sampler_type("ksampler") is True


def test_is_named_sampler_iterative():
    assert p._is_named_sampler_type("iterativelatentupscale") is True


def test_is_named_sampler_marigold():
    assert p._is_named_sampler_type("marigold_depth") is True


def test_is_named_sampler_flux_params():
    assert p._is_named_sampler_type("fluxparams") is True


def test_is_named_sampler_flux_sampler():
    assert p._is_named_sampler_type("fluxsampler") is True


def test_is_named_sampler_flux2():
    assert p._is_named_sampler_type("flux2") is True


def test_is_named_sampler_flux_2():
    assert p._is_named_sampler_type("my_flux_2_node") is True


def test_is_named_sampler_unrelated():
    assert p._is_named_sampler_type("saveimage") is False


# ─── _has_core_sampler_signature ───────────────────────────────────────────

def test_has_core_sampler_signature_exception():
    # Pass something where _inputs() would fail
    assert p._has_core_sampler_signature({"inputs": None}) is False


def test_has_core_sampler_signature_missing_cfg():
    node = {"inputs": {"steps": 10, "seed": 1}}
    assert p._has_core_sampler_signature(node) is False


def test_has_core_sampler_signature_complete():
    node = {"inputs": {"steps": 10, "cfg": 7.0, "seed": 1}}
    assert p._has_core_sampler_signature(node) is True


def test_has_core_sampler_signature_guidance():
    node = {"inputs": {"steps": 10, "guidance": 3.5, "noise_seed": 42}}
    assert p._has_core_sampler_signature(node) is True


# ─── _is_custom_sampler ────────────────────────────────────────────────────

def test_is_custom_sampler_no_sampler_in_ct():
    assert p._is_custom_sampler({"inputs": {}}, "saveimage") is False


def test_is_custom_sampler_select_in_ct():
    assert p._is_custom_sampler({"inputs": {}}, "mysamplerselect") is False


def test_is_custom_sampler_with_model_link():
    node = {"inputs": {"model": ["1", 0]}}
    assert p._is_custom_sampler(node, "mysampler") is True


def test_is_custom_sampler_with_text_embeds_link():
    node = {"inputs": {"text_embeds": ["2", 0]}}
    assert p._is_custom_sampler(node, "videosampler") is True


def test_is_custom_sampler_with_hyvid_embeds():
    node = {"inputs": {"hyvid_embeds": ["3", 0]}}
    assert p._is_custom_sampler(node, "hyvidsampler") is True


def test_is_custom_sampler_inputs_exception():
    # inputs=None triggers except path
    assert p._is_custom_sampler({"inputs": None}, "sampler") is False


# ─── _is_advanced_sampler ──────────────────────────────────────────────────

def test_is_advanced_sampler_no_ct():
    assert p._is_advanced_sampler({}) is False


def test_is_advanced_sampler_samplercustom():
    assert p._is_advanced_sampler({"class_type": "SamplerCustomAdvanced", "inputs": {}}) is True


def test_is_advanced_sampler_guider_sigmas():
    node = {"class_type": "FlowSampler", "inputs": {"guider": ["1", 0], "sigmas": ["2", 0]}}
    assert p._is_advanced_sampler(node) is True


def test_is_advanced_sampler_guider_sampler():
    node = {"class_type": "FlowSampler", "inputs": {"guider": ["1", 0], "sampler": ["2", 0]}}
    assert p._is_advanced_sampler(node) is True


def test_is_advanced_sampler_all_four():
    node = {"class_type": "X", "inputs": {
        "noise": ["1", 0], "guider": ["2", 0], "sampler": ["3", 0], "sigmas": ["4", 0]
    }}
    assert p._is_advanced_sampler(node) is True


def test_is_advanced_sampler_exception_path():
    # inputs=None → exception → False
    assert p._is_advanced_sampler({"class_type": "X", "inputs": None}) is False


# ─── _extract_posneg_from_text_embeds ──────────────────────────────────────

def test_extract_posneg_walk_returns_none():
    pos, neg = p._extract_posneg_from_text_embeds({}, None)
    assert pos is None and neg is None


def test_extract_posneg_node_not_found():
    nodes = {"1": {"class_type": "X", "inputs": {}}}
    pos, neg = p._extract_posneg_from_text_embeds(nodes, ["99", 0])
    assert pos is None and neg is None


def test_extract_posneg_with_prompts():
    nodes = {
        "1": {"class_type": "WanVideoTextEncode", "inputs": {
            "positive_prompt": "beautiful landscape",
            "negative_prompt": "ugly",
        }}
    }
    pos, neg = p._extract_posneg_from_text_embeds(nodes, ["1", 0])
    assert pos is not None and pos[0] == "beautiful landscape"
    assert neg is not None and neg[0] == "ugly"


def test_extract_posneg_no_prompts():
    nodes = {"2": {"class_type": "EmptyNode", "inputs": {"other": 1}}}
    pos, neg = p._extract_posneg_from_text_embeds(nodes, ["2", 0])
    assert pos is None and neg is None


# ─── _select_primary_sampler / _select_advanced_sampler ────────────────────

def test_select_primary_sampler_no_sink():
    nodes = {"1": {"class_type": "NoSink", "inputs": {}}}
    sid, conf = p._select_primary_sampler(nodes, "1")
    assert sid is None


def test_select_advanced_sampler_no_sink():
    nodes = {}
    sid, conf = p._select_advanced_sampler(nodes, "99")
    assert sid is None


# ─── _select_sampler_from_sink / _sink_start_source ────────────────────────

def test_sink_start_source_not_dict():
    assert p._sink_start_source({}, "99") is None


def test_sink_start_source_no_input_link():
    nodes = {"1": {"class_type": "SaveImage", "inputs": {}}}
    assert p._sink_start_source(nodes, "1") is None


def test_sink_start_source_with_link():
    nodes = {
        "1": {"class_type": "SaveImage", "inputs": {"images": ["2", 0]}},
        "2": {"class_type": "KSampler", "inputs": {"steps": 20, "cfg": 7, "seed": 1}},
    }
    src = p._sink_start_source(nodes, "1")
    assert src == "2"


# ─── _best_candidate ───────────────────────────────────────────────────────

def test_best_candidate_empty():
    assert p._best_candidate([]) == (None, "none")


def test_best_candidate_single():
    assert p._best_candidate([("n1", 0)]) == ("n1", "high")


def test_best_candidate_multiple_same_depth():
    result = p._best_candidate([("n1", 2), ("n2", 2)])
    assert result[1] == "medium"


def test_best_candidate_multiple_different_depth():
    result = p._best_candidate([("n1", 3), ("n2", 1)])
    assert result[0] == "n2" and result[1] == "high"


# ─── _select_any_sampler ───────────────────────────────────────────────────

def test_select_any_sampler_no_candidates():
    nodes = {"1": {"class_type": "SaveImage", "inputs": {}}}
    nid, conf = p._select_any_sampler(nodes)
    assert nid is None and conf == "none"


def test_select_any_sampler_with_ksampler():
    nodes = {
        "5": {"class_type": "KSampler", "inputs": {"steps": 20, "cfg": 7, "seed": 1, "model": ["0", 0], "positive": ["1", 0]}},
    }
    nid, conf = p._select_any_sampler(nodes)
    assert nid == "5" and conf == "low"


# ─── _sampler_candidate_score ──────────────────────────────────────────────

def test_sampler_candidate_score_empty():
    assert p._sampler_candidate_score({}) == 0


def test_sampler_candidate_score_model_positive():
    ins = {"model": ["1", 0], "positive": ["2", 0], "steps": 20}
    score = p._sampler_candidate_score(ins)
    assert score >= 7


# ─── _stable_numeric_node_id ───────────────────────────────────────────────

def test_stable_numeric_node_id_valid():
    assert p._stable_numeric_node_id("42") == 42


def test_stable_numeric_node_id_invalid():
    assert p._stable_numeric_node_id("abc") == 10 ** 9


# ─── _trace_sampler_name ───────────────────────────────────────────────────

def test_trace_sampler_name_no_walk():
    assert p._trace_sampler_name({}, None) is None


def test_trace_sampler_name_node_not_dict():
    nodes = {}
    assert p._trace_sampler_name(nodes, ["99", 0]) is None


def test_trace_sampler_name_found():
    nodes = {"1": {"class_type": "KSamplerSelect", "inputs": {"sampler_name": "euler"}}}
    result = p._trace_sampler_name(nodes, ["1", 0])
    assert result is not None and result[0] == "euler"


# ─── _trace_noise_seed ─────────────────────────────────────────────────────

def test_trace_noise_seed_no_walk():
    assert p._trace_noise_seed({}, None) is None


def test_trace_noise_seed_node_not_dict():
    assert p._trace_noise_seed({}, ["99", 0]) is None


def test_trace_noise_seed_found():
    nodes = {"3": {"class_type": "RandomNoise", "inputs": {"noise_seed": 12345}}}
    result = p._trace_noise_seed(nodes, ["3", 0])
    assert result is not None and result[0] == 12345


# ─── _trace_scheduler_sigmas ───────────────────────────────────────────────

def test_trace_scheduler_sigmas_no_walk():
    result = p._trace_scheduler_sigmas({}, None)
    assert result[0] is None and result[1] is None


def test_trace_scheduler_sigmas_found():
    nodes = {
        "5": {"class_type": "BasicScheduler", "inputs": {"steps": 25, "scheduler": "karras", "denoise": 1.0}}
    }
    steps, scheduler, denoise, model_link, src_tuple, steps_conf = p._trace_scheduler_sigmas(nodes, ["5", 0])
    assert steps == 25 and scheduler == "karras" and denoise == 1.0


# ─── _steps_from_manual_sigmas ─────────────────────────────────────────────

def test_steps_from_manual_sigmas_too_few():
    ins = {"sigmas": "0.9"}
    steps, conf = p._steps_from_manual_sigmas(ins)
    assert steps is None


def test_steps_from_manual_sigmas_enough():
    ins = {"sigmas": "1.0, 0.7, 0.4, 0.1"}
    steps, conf = p._steps_from_manual_sigmas(ins)
    assert steps == 3  # 4 values → max(1, 4-1)


# ─── _count_numeric_sigma_values ───────────────────────────────────────────

def test_count_numeric_sigma_values_non_string():
    assert p._count_numeric_sigma_values(None) == 0
    assert p._count_numeric_sigma_values(42) == 0


def test_count_numeric_sigma_values_csv():
    assert p._count_numeric_sigma_values("1.0, 0.5, 0.2") == 3


def test_count_numeric_sigma_values_mixed():
    assert p._count_numeric_sigma_values("1.0, bad, 0.2") == 2


# ─── _trace_guidance_from_conditioning ─────────────────────────────────────

def test_trace_guidance_from_conditioning_no_walk():
    assert p._trace_guidance_from_conditioning({}, None) is None


def test_trace_guidance_from_conditioning_found():
    nodes = {
        "1": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["9", 0], "text": "hi", "guidance": 3.5}},
    }
    result = p._trace_guidance_from_conditioning(nodes, ["1", 0])
    assert result is not None and result[0] == 3.5


# ─── _scalar ───────────────────────────────────────────────────────────────

def test_scalar_none():
    assert p._scalar(None) is None


def test_scalar_int():
    assert p._scalar(42) == 42


def test_scalar_float():
    assert p._scalar(3.14) == 3.14


def test_scalar_list():
    assert p._scalar([1, 2]) is None


# ─── _extract_ksampler_widget_params ───────────────────────────────────────

def test_extract_ksampler_widget_params_not_dict():
    assert p._extract_ksampler_widget_params("not_a_dict") == {}


def test_extract_ksampler_widget_params_not_ksampler():
    node = {"class_type": "SaveImage", "inputs": {}}
    assert p._extract_ksampler_widget_params(node) == {}


def test_extract_ksampler_widget_params_no_widgets():
    node = {"class_type": "KSampler", "inputs": {}}
    assert p._extract_ksampler_widget_params(node) == {}


def test_extract_ksampler_widget_params_full():
    node = {"class_type": "KSampler", "widgets_values": [111, "ctrl", 20, 7.5, "euler", "normal", 0.9]}
    out = p._extract_ksampler_widget_params(node)
    assert out["seed"] == 111 and out["steps"] == 20 and out["cfg"] == 7.5
    assert out["sampler_name"] == "euler"


# ─── _ksampler_values_from_widgets ─────────────────────────────────────────

def test_ksampler_values_from_widgets_short():
    out = p._ksampler_values_from_widgets([42])
    assert out["seed"] == 42
    assert "steps" not in out  # index 2 missing


# ─── _extract_lyrics_from_prompt_nodes ─────────────────────────────────────

def test_extract_lyrics_from_prompt_nodes_none():
    nodes = {"1": {"class_type": "SaveImage", "inputs": {}}}
    lyr, strength, src = p._extract_lyrics_from_prompt_nodes(nodes)
    assert lyr is None


def test_extract_lyrics_from_prompt_nodes_with_acestep():
    nodes = {
        "1": {
            "class_type": "AceStep15TaskTextEncode",
            "inputs": {"task_text": "hello lyrics", "lyrics_strength": 0.8},
        }
    }
    lyr, strength, src = p._extract_lyrics_from_prompt_nodes(nodes)
    assert lyr == "hello lyrics"
    assert strength == 0.8


# ─── _extract_lyrics_from_inputs ───────────────────────────────────────────

def test_extract_lyrics_from_inputs_no_key():
    assert p._extract_lyrics_from_inputs({}, "sometextencode") is None


def test_extract_lyrics_from_inputs_lyrics_key():
    assert p._extract_lyrics_from_inputs({"lyrics": "song text"}, "textnode") == "song text"


# ─── _extract_lyrics_strength ──────────────────────────────────────────────

def test_extract_lyrics_strength_missing():
    assert p._extract_lyrics_strength({}) is None


def test_extract_lyrics_strength_present():
    assert p._extract_lyrics_strength({"lyrics_strength": 0.75}) == 0.75


# ─── _first_non_empty_string / _first_non_none_scalar ──────────────────────

def test_first_non_empty_string_not_found():
    assert p._first_non_empty_string({}, ("a", "b")) is None


def test_first_non_empty_string_found():
    assert p._first_non_empty_string({"b": "hi"}, ("a", "b")) == "hi"


def test_first_non_none_scalar_not_found():
    assert p._first_non_none_scalar({}, ("a", "b")) is None


def test_first_non_none_scalar_found():
    assert p._first_non_none_scalar({"a": 5}, ("a", "b")) == 5


# ─── _resolve_scalar_from_link ─────────────────────────────────────────────

def test_resolve_scalar_from_link_no_walk():
    assert p._resolve_scalar_from_link({}, None) is None


def test_resolve_scalar_from_link_node_not_dict():
    assert p._resolve_scalar_from_link({}, ["99", 0]) is None


def test_resolve_scalar_from_link_found():
    nodes = {"4": {"class_type": "Primitive", "inputs": {"value": 77}}}
    result = p._resolve_scalar_from_link(nodes, ["4", 0])
    assert result == 77


# ─── _extract_text ─────────────────────────────────────────────────────────

def test_extract_text_no_walk():
    assert p._extract_text({}, None) is None


def test_extract_text_node_not_dict():
    assert p._extract_text({}, ["99", 0]) is None


def test_extract_text_no_candidates():
    nodes = {"5": {"class_type": "Other", "inputs": {"x": 1}}}
    assert p._extract_text(nodes, ["5", 0]) is None


def test_extract_text_found():
    nodes = {"6": {"class_type": "CLIPTextEncode", "inputs": {"text": "my prompt", "clip": ["9", 0]}}}
    result = p._extract_text(nodes, ["6", 0])
    assert result is not None and "my prompt" in result[0]


# ─── _looks_like_conditioning_text ─────────────────────────────────────────

def test_looks_like_conditioning_text_wrong_ct():
    assert p._looks_like_conditioning_text({"class_type": "LoadImage", "inputs": {"text": "x" * 10}}) is False


def test_looks_like_conditioning_text_ok():
    node = {"class_type": "PromptNode", "inputs": {"text": "a nice prompt"}}
    assert p._looks_like_conditioning_text(node) is True


# ─── _conditioning_key_allowed ─────────────────────────────────────────────

def test_conditioning_key_allowed_positive_branch_rejects_negative():
    assert p._conditioning_key_allowed("negative_prompt", "positive") is False


def test_conditioning_key_allowed_negative_branch_rejects_positive():
    assert p._conditioning_key_allowed("positive_prompt", "negative") is False


def test_conditioning_key_allowed_no_branch():
    assert p._conditioning_key_allowed("anything", None) is True


# ─── _conditioning_should_expand ───────────────────────────────────────────

def test_conditioning_should_expand_zeroout_blocked():
    node = {"class_type": "ConditioningZeroOut", "inputs": {}}
    assert p._conditioning_should_expand(node, "negative") is False


def test_conditioning_should_expand_reroute():
    node = {"class_type": "Reroute", "inputs": {"x": ["1", 0]}}
    assert p._conditioning_should_expand(node, None) is True


def test_conditioning_should_expand_has_conditioning_input():
    node = {"class_type": "SomeNode", "inputs": {"conditioning": ["1", 0]}}
    assert p._conditioning_should_expand(node, None) is True


# ─── _select_sampler_context ───────────────────────────────────────────────

def test_select_sampler_context_no_sinks():
    nodes = {"1": {"class_type": "NoSink", "inputs": {}}}
    sid, conf, mode = p._select_sampler_context(nodes, "1")
    assert mode in {"primary", "advanced", "global", "fallback"}
