from mjr_am_backend.features.geninfo import api_node_extractor as x
from mjr_am_backend.features.geninfo import parser_impl as p

# ---------------------------------------------------------------------------
# Detection helpers
# ---------------------------------------------------------------------------

def test_detect_api_node_known_providers():
    assert x._detect_api_node("geminiimagenode") == "google_gemini"
    assert x._detect_api_node("openaigptimage1") == "openai"
    assert x._detect_api_node("openaiimagenode") == "openai"
    assert x._detect_api_node("claudenode") == "anthropic"
    assert x._detect_api_node("bytedanceseedancenode") == "bytedance_seedance"
    assert x._detect_api_node("klingtexttovideo") == "kling_ai"
    assert x._detect_api_node("lumaimagetovideo") == "luma_dream_machine"
    assert x._detect_api_node("minimaxhailuo") == "minimax_hailuo"
    assert x._detect_api_node("runwayimagetovideo") == "runway"
    assert x._detect_api_node("vidutexttovideo") == "vidu"
    assert x._detect_api_node("moonvalleytxt2video") == "moonvalley"
    assert x._detect_api_node("pixversetexttovideo") == "pixverse"
    assert x._detect_api_node("ideogramv2") == "ideogram"
    assert x._detect_api_node("recraftv4") == "recraft"
    assert x._detect_api_node("grokimage") == "xai_grok"
    assert x._detect_api_node("ltxvapitext") == "ltxv_api"
    assert x._detect_api_node("elevenlabstextto") == "eleven_labs"
    assert x._detect_api_node("wan2texttovideoapi") == "alibaba_wan"
    assert x._detect_api_node("happyhorseimagetovideoapi") == "happy_horse"
    assert x._detect_api_node("fluxkontext") == "black_forest_labs"
    assert x._detect_api_node("stabilitystableimage") == "stability_ai"
    assert x._detect_api_node("unknownnode") is None


def test_detect_api_node_specificity_order():
    # More specific fragments must match before the broad catch-all
    assert x._detect_api_node("bytedanceseedream") == "bytedance_seedance"
    assert x._detect_api_node("bytedance2referencenode") == "bytedance_seedance"
    assert x._detect_api_node("geminiimage2node") == "google_gemini"
    assert x._detect_api_node("veo3videogeneration") == "google_veo"


def test_provider_from_model_name():
    assert x._provider_from_model_name("seedance-2.0-pro") == "bytedance_seedance"
    assert x._provider_from_model_name("gemini-2.0-flash") == "google_gemini"
    assert x._provider_from_model_name("veo-3") == "google_veo"
    assert x._provider_from_model_name("gpt-4o") == "openai"
    assert x._provider_from_model_name("dall-e-3") == "openai"
    assert x._provider_from_model_name("claude-3-opus") == "anthropic"
    assert x._provider_from_model_name("flux-kontext-pro") == "black_forest_labs"
    assert x._provider_from_model_name("stable-diffusion-xl") == "stability_ai"
    assert x._provider_from_model_name("happyhorse-1.0-i2v") == "happy_horse"
    assert x._provider_from_model_name("wan-2.1") == "alibaba_wan"
    assert x._provider_from_model_name("kling-v2") == "kling_ai"
    assert x._provider_from_model_name("luma-dream-machine") == "luma_dream_machine"
    assert x._provider_from_model_name("minimax-01") == "minimax_hailuo"
    assert x._provider_from_model_name("runway-gen3") == "runway"
    assert x._provider_from_model_name("vidu-q1") == "vidu"
    assert x._provider_from_model_name("ideogram-v3") == "ideogram"
    assert x._provider_from_model_name("recraft-v3") == "recraft"
    assert x._provider_from_model_name("grok-aurora") == "xai_grok"
    assert x._provider_from_model_name("pixverse-v4") == "pixverse"
    assert x._provider_from_model_name("unknown-model-xyz") is None


def test_provider_from_model_name_wan_no_media_type_required():
    # Wan provider must be detected even without "video" or "image" in name
    assert x._provider_from_model_name("wan-2.1-14b") == "alibaba_wan"
    assert x._provider_from_model_name("wanx-turbo") == "alibaba_wan"


# ---------------------------------------------------------------------------
# Node classification
# ---------------------------------------------------------------------------

def test_is_api_image_node_known_class_type():
    node = {"class_type": "GeminiImageNode", "inputs": {}}
    assert x._is_api_image_node(node) is True


def test_is_api_image_node_heuristic_fallback():
    node = {
        "class_type": "CustomExternalGen",
        "inputs": {"prompt": "a cat", "model": "custom-v1"},
    }
    assert x._is_api_image_node(node) is True


def test_is_api_image_node_heuristic_rejects_sampler_node():
    node = {
        "class_type": "CustomSampler",
        "inputs": {"prompt": "a cat", "model": "v1", "cfg": 7.5, "steps": 20},
    }
    assert x._is_api_image_node(node) is False


def test_is_api_image_node_rejects_non_api():
    node = {"class_type": "CLIPTextEncode", "inputs": {"text": "hello"}}
    assert x._is_api_image_node(node) is False


# ---------------------------------------------------------------------------
# Field extraction helpers
# ---------------------------------------------------------------------------

def test_extract_api_node_prompt_priority():
    assert x._extract_api_node_prompt({"prompt": "a cat"}) == "a cat"
    assert x._extract_api_node_prompt({"model.prompt": "a galloping horse"}) == "a galloping horse"
    assert x._extract_api_node_prompt({"text": "a dog"}) == "a dog"
    assert x._extract_api_node_prompt({"positive_prompt": "sun"}) == "sun"
    assert x._extract_api_node_prompt({}) is None
    assert x._extract_api_node_prompt({"prompt": "  "}) is None


def test_extract_api_node_negative():
    assert x._extract_api_node_negative({"negative_prompt": "blur"}) == "blur"
    assert x._extract_api_node_negative({"model.negative_prompt": "washed out"}) == "washed out"
    assert x._extract_api_node_negative({"negative": "ugly"}) == "ugly"
    assert x._extract_api_node_negative({}) is None


def test_extract_api_node_prompt_resolves_linked_text():
    nodes = {
        "1": {"class_type": "PrimitiveStringMultiline", "inputs": {"value": "galloping chrome horse"}},
    }
    assert x._extract_api_node_prompt({"model.prompt": ["1", 0]}, nodes) == "galloping chrome horse"


def test_extract_api_node_model():
    node = {"class_type": "GeminiImageNode"}
    m = x._extract_api_node_model({"model": "gemini-2.0-flash"}, "1", node)
    assert m is not None
    assert m["name"] == "gemini-2.0-flash"
    assert m["confidence"] == "high"
    assert x._extract_api_node_model({}, "1", node) is None
    assert x._extract_api_node_model({"model": "  "}, "1", node) is None


def test_extract_api_system_prompt():
    assert x._extract_api_system_prompt({"system_prompt": "be concise"}) == "be concise"
    assert x._extract_api_system_prompt({"system": "you are helpful"}) == "you are helpful"
    assert x._extract_api_system_prompt({}) is None


# ---------------------------------------------------------------------------
# Engine type refinement
# ---------------------------------------------------------------------------

def test_refine_engine_type_txt2img():
    out = {"engine": {"type": "txt2img"}}
    x._refine_engine_type(out, "openaitexttoimageapi", {})
    assert out["engine"]["type"] == "txt2img"


def test_refine_engine_type_img2vid():
    out = {"engine": {"type": "txt2vid"}}
    x._refine_engine_type(out, "klingimagetovideo", {})
    assert out["engine"]["type"] == "img2vid"


def test_refine_engine_type_img2vid_from_video_with_image_input():
    out = {"engine": {"type": "txt2vid"}}
    x._refine_engine_type(out, "somevideogen", {"image": ["1", 0]})
    assert out["engine"]["type"] == "img2vid"


def test_refine_engine_type_txt2vid_from_video_no_image_input():
    out = {"engine": {"type": "txt2img"}}
    x._refine_engine_type(out, "somevideogen", {"prompt": "a sunset"})
    assert out["engine"]["type"] == "txt2vid"


def test_refine_engine_type_audio():
    out = {"engine": {"type": "txt2img"}}
    x._refine_engine_type(out, "sonilotexttomusic", {})
    assert out["engine"]["type"] == "audio"


# ---------------------------------------------------------------------------
# Seedance helpers
# ---------------------------------------------------------------------------

def test_seedance_size_from_resolution():
    assert x._seedance_size_from_resolution("720p") == (1280, 720)
    assert x._seedance_size_from_resolution("1080p") == (1920, 1080)
    assert x._seedance_size_from_resolution("480p") == (854, 480)
    assert x._seedance_size_from_resolution("4k") == (3840, 2160)
    assert x._seedance_size_from_resolution("360p") is None


def test_seedance_media_type_txt2vid():
    assert x._seedance_media_type({"prompt": "a cat"}) == "txt2vid"


def test_seedance_media_type_img2vid():
    ins = {"model.reference_images": ["1", 0]}
    assert x._seedance_media_type(ins) == "img2vid"


# ---------------------------------------------------------------------------
# Full fallback entry-point
# ---------------------------------------------------------------------------

def _make_gemini_nodes(prompt="a golden retriever in sunlight", model="gemini-2.0-flash-preview-image-generation"):
    return {
        "1": {
            "class_type": "GeminiImageNode",
            "inputs": {"prompt": prompt, "model": model, "seed": 42},
        },
        "2": {"class_type": "SaveImage", "inputs": {"images": ["1", 0]}},
    }


def test_extract_api_node_geninfo_fallback_basic():
    nodes = _make_gemini_nodes()
    result = x._extract_api_node_geninfo_fallback(nodes, None)
    assert result is not None
    assert result["positive"]["value"] == "a golden retriever in sunlight"
    assert result["engine"]["api_provider"] == "google_gemini"
    assert result["engine"]["sampler_mode"] == "api"
    assert result["checkpoint"]["name"] == "gemini-2.0-flash-preview-image-generation"
    assert result["seed"]["value"] == 42


def test_extract_api_node_geninfo_fallback_no_api_nodes():
    nodes = {
        "1": {"class_type": "CLIPTextEncode", "inputs": {"text": "hello"}},
    }
    assert x._extract_api_node_geninfo_fallback(nodes, None) is None


def test_extract_api_node_geninfo_fallback_returns_none_if_no_meaningful_fields():
    # Node is detected as API type but has no extractable data
    nodes = {"1": {"class_type": "GeminiImageNode", "inputs": {}}}
    result = x._extract_api_node_geninfo_fallback(nodes, None)
    assert result is None


def test_extract_api_node_geninfo_fallback_with_workflow_meta():
    nodes = _make_gemini_nodes()
    meta = {"title": "Test Workflow"}
    result = x._extract_api_node_geninfo_fallback(nodes, meta)
    assert result is not None
    assert result.get("metadata") == meta


def test_extract_api_node_geninfo_fallback_multiple_nodes():
    nodes = {
        "1": {
            "class_type": "GeminiImageNode",
            "inputs": {"prompt": "first prompt", "model": "gemini-2.0"},
        },
        "2": {
            "class_type": "OpenAIImageNode",
            "inputs": {"prompt": "second prompt", "model": "dall-e-3"},
        },
        "3": {"class_type": "SaveImage", "inputs": {"images": ["1", 0]}},
    }
    result = x._extract_api_node_geninfo_fallback(nodes, None)
    assert result is not None
    extra = result.get("extra_api_passes", [])
    assert len(extra) == 1
    assert extra[0]["positive"]["value"] == "second prompt"


def test_extract_api_node_geninfo_fallback_openai():
    nodes = {
        "1": {
            "class_type": "OpenAIImageNode",
            "inputs": {"prompt": "a serene mountain lake", "model": "gpt-image-1", "seed": 7},
        },
    }
    result = x._extract_api_node_geninfo_fallback(nodes, None)
    assert result is not None
    assert result["engine"]["api_provider"] == "openai"
    assert result["positive"]["value"] == "a serene mountain lake"


def test_extract_api_node_geninfo_fallback_openai_gpt_image_linked_prompt():
    nodes = {
        "1": {"class_type": "PrimitiveStringMultiline", "inputs": {"value": "shoe"}},
        "2": {
            "class_type": "StringReplace",
            "inputs": {
                "string": "Create a product technical analysis infographic for [YOUR PRODUCT].",
                "find": "[YOUR PRODUCT]",
                "replace": ["1", 0],
            },
        },
        "3": {
            "class_type": "OpenAIGPTImage1",
            "inputs": {
                "prompt": ["2", 0],
                "seed": 126220530,
                "quality": "low",
                "background": "auto",
                "size": "1024x1024",
                "n": 1,
                "model": "gpt-image-2",
                "custom_width": 1024,
                "custom_height": 1024,
                "image": ["4", 0],
            },
        },
        "4": {"class_type": "LoadImage", "inputs": {"image": "reference.png"}},
        "5": {"class_type": "SaveImage", "inputs": {"images": ["3", 0]}},
    }
    result = x._extract_api_node_geninfo_fallback(nodes, None)
    assert result is not None
    assert result["engine"]["api_provider"] == "openai"
    assert result["engine"]["type"] == "txt2img"
    assert result["positive"]["value"] == "Create a product technical analysis infographic for shoe."
    assert result["checkpoint"]["name"] == "gpt-image-2"
    assert result["seed"]["value"] == 126220530
    assert result["quality"]["value"] == "low"
    assert result["batch_size"]["value"] == 1
    assert result["size"]["width"] == 1024
    assert result["size"]["height"] == 1024
    assert result["inputs"][0]["filename"] == "reference.png"


def test_extract_api_node_geninfo_fallback_seedance_dotted_keys():
    nodes = {
        "1": {
            "class_type": "ByteDanceSeedanceNode",
            "inputs": {
                "model": "Seedance 2.0",
                "model.prompt": "cinematic ocean sunset",
                "model.negative_prompt": "blur, noisy",
                "model.resolution": "1080p",
                "model.duration": 5,
                "seed": 99,
            },
        },
    }
    result = x._extract_api_node_geninfo_fallback(nodes, None)
    assert result is not None
    assert result["engine"]["api_provider"] == "bytedance_seedance"
    assert result["positive"]["value"] == "cinematic ocean sunset"
    assert result["negative"]["value"] == "blur, noisy"
    assert result["size"]["width"] == 1920
    assert result["seed"]["value"] == 99


def test_extract_api_node_geninfo_fallback_happy_horse_dotted_keys():
    nodes = {
        "13": {
            "class_type": "HappyHorseImageToVideoApi",
            "inputs": {
                "model": "happyhorse-1.0-i2v",
                "model.prompt": "a happy horse running in a meadow",
                "model.resolution": "720p",
                "model.duration": 5,
                "seed": 962403639,
                "first_frame": ["14", 0],
            },
        },
        "14": {
            "class_type": "LoadImage",
            "inputs": {"image": "horse.png"},
        },
    }
    result = x._extract_api_node_geninfo_fallback(nodes, None)
    assert result is not None
    assert result["engine"]["api_provider"] == "happy_horse"
    assert result["engine"]["type"] == "img2vid"
    assert result["positive"]["value"] == "a happy horse running in a meadow"
    assert result["checkpoint"]["name"] == "happyhorse-1.0-i2v"
    assert result["resolution"]["value"] == "720p"
    assert result["size"]["width"] == 1280
    assert result["size"]["height"] == 720
    assert result["duration"]["value"] == 5
    assert result["seed"]["value"] == 962403639


def test_extract_api_node_geninfo_fallback_with_negative_prompt():
    nodes = {
        "1": {
            "class_type": "StabilityStableImage",
            "inputs": {
                "prompt": "portrait of a warrior",
                "negative_prompt": "ugly, deformed",
                "model": "stable-diffusion-xl-1024-v1-0",
            },
        },
    }
    result = x._extract_api_node_geninfo_fallback(nodes, None)
    assert result is not None
    assert result["negative"]["value"] == "ugly, deformed"


def test_extract_api_node_geninfo_fallback_size_from_width_height():
    nodes = {
        "1": {
            "class_type": "RecraftV4",
            "inputs": {"prompt": "abstract art", "model": "recraft-v3", "width": 1024, "height": 768},
        },
    }
    result = x._extract_api_node_geninfo_fallback(nodes, None)
    assert result is not None
    assert result["size"]["width"] == 1024
    assert result["size"]["height"] == 768


def test_extract_api_node_geninfo_fallback_wan_provider():
    nodes = {
        "1": {
            "class_type": "Wan2TextToVideoAPI",
            "inputs": {"prompt": "a butterfly emerging from cocoon", "model": "wan-2.1"},
        },
    }
    result = x._extract_api_node_geninfo_fallback(nodes, None)
    assert result is not None
    assert result["engine"]["api_provider"] == "alibaba_wan"


def test_extract_api_node_geninfo_fallback_aspect_ratio():
    nodes = {
        "1": {
            "class_type": "KlingTextToVideo",
            "inputs": {"prompt": "flying eagle", "model": "kling-v2", "aspect_ratio": "16:9"},
        },
    }
    result = x._extract_api_node_geninfo_fallback(nodes, None)
    assert result is not None
    assert result.get("aspect_ratio", {}).get("value") == "16:9"


def test_extract_api_node_geninfo_fallback_full_integration_via_parser():
    """End-to-end: parse_geninfo_from_prompt must surface API node data when no sampler present."""
    nodes = {
        "1": {
            "class_type": "GeminiImageNode",
            "inputs": {"prompt": "a red fox in a snowy forest", "model": "gemini-2.0-flash", "seed": 12},
        },
        "2": {"class_type": "SaveImage", "inputs": {"images": ["1", 0]}},
    }
    res = p.parse_geninfo_from_prompt(nodes)
    assert res.ok
    data = res.data
    assert isinstance(data, dict)
    assert data.get("positive", {}).get("value") == "a red fox in a snowy forest"
    assert data.get("engine", {}).get("api_provider") == "google_gemini"


def test_parse_geninfo_from_prompt_surfaces_linked_happy_horse_prompt():
    nodes = {
        "1": {"class_type": "PrimitiveStringMultiline", "inputs": {"value": "cinematic happy horse across a foggy valley"}},
        "13": {
            "class_type": "HappyHorseImageToVideoApi",
            "inputs": {
                "model": "happyhorse-1.0-i2v",
                "model.prompt": ["1", 0],
                "model.resolution": "720P",
                "model.duration": 5,
                "seed": 962403639,
                "first_frame": ["14", 0],
            },
        },
        "14": {"class_type": "LoadImage", "inputs": {"image": "horse.png"}},
        "15": {"class_type": "VHS_VideoCombine", "inputs": {"images": ["13", 0]}},
    }

    res = p.parse_geninfo_from_prompt(nodes)
    assert res.ok
    data = res.data
    assert isinstance(data, dict)
    assert data.get("engine", {}).get("api_provider") == "happy_horse"
    assert data.get("positive", {}).get("value") == "cinematic happy horse across a foggy valley"
    assert data.get("checkpoint", {}).get("name") == "happyhorse-1.0-i2v"
