Ctrl K
ComunidadGuías…

tests/test_csharp_type_resolution.py

Rama v8 · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
from __future__ import annotations

from pathlib import Path

from graphify.extract import extract


def _write(path: Path, text: str) -> Path:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")
    return path


def _node_by_id(result: dict, nid: str) -> dict | None:
    return next((n for n in result["nodes"] if n.get("id") == nid), None)


def _targets(result: dict, relation: str, label: str) -> list[dict]:
    out = []
    for e in result["edges"]:
        if e.get("relation") != relation:
            continue
        n = _node_by_id(result, e.get("target"))
        if n is not None and n.get("label") == label:
            out.append(n)
    return out


def _defs(result: dict, label: str) -> list[dict]:
    return [
        n for n in result["nodes"]
        if n.get("label") == label and n.get("source_file")
    ]


def test_csharp_declaration_nodes_carry_enclosing_namespace(tmp_path: Path):
    block = _write(
        tmp_path / "block.cs",
        "namespace Game.Core { public class Damage {} }\n",
    )
    nested = _write(
        tmp_path / "nested.cs",
        "namespace Outer { namespace Inner { public class NestedDamage {} } }\n",
    )
    file_scoped = _write(
        tmp_path / "file_scoped.cs",
        "namespace FileScoped.Core;\npublic class FileScopedDamage {}\n",
    )
    result = extract([block, nested, file_scoped], cache_root=tmp_path)

    assert _defs(result, "Damage")[0].get("metadata", {}).get("namespace") == "Game.Core"
    assert _defs(result, "NestedDamage")[0].get("metadata", {}).get("namespace") == "Outer.Inner"
    assert _defs(result, "FileScopedDamage")[0].get("metadata", {}).get("namespace") == "FileScoped.Core"
    assert _defs(result, "Damage")[0]["metadata"].get("scope_chain"), "lexical scope_chain must be stamped"


def test_csharp_cross_file_inherits_resolves_to_real_def(tmp_path: Path):
    core = _write(tmp_path / "core.cs",
                  "namespace Game.Core { public class Damage { public int Calc() { return 1; } } }\n")
    combat = _write(tmp_path / "combat.cs",
                    "using Game.Core;\nnamespace Game.Combat { public class Weapon : Damage {} }\n")
    result = extract([core, combat], cache_root=tmp_path)

    damage = _targets(result, "inherits", "Damage")
    assert damage, "expected an inherits edge to Damage"
    assert all(d.get("source_file") for d in damage), \
        "Weapon : Damage must resolve to the real Damage def, not a shadow stub"


def test_csharp_collision_disambiguated_by_using(tmp_path: Path):
    core = _write(tmp_path / "core.cs",
                  "namespace Game.Core { public class WeaponData { public int Number; } }\n")
    ui = _write(tmp_path / "ui.cs",
                "namespace Game.UI { public class WeaponData { public int Width; } }\n")
    combat = _write(tmp_path / "combat.cs",
                    "using Game.Core;\nnamespace Game.Combat { public class Holder { public WeaponData data; } }\n")
    result = extract([core, ui, combat], cache_root=tmp_path)

    shadow = [n for n in result["nodes"]
              if n.get("label") == "WeaponData" and not n.get("source_file")]
    assert not shadow, f"orphan WeaponData shadow node(s) remain: {[n['id'] for n in shadow]}"

    resolved = [w for w in _targets(result, "references", "WeaponData") if w.get("source_file")]
    assert resolved, "WeaponData reference should resolve to a real def"
    assert all("core.cs" in w["source_file"] for w in resolved), \
        "must disambiguate to Game.Core.WeaponData via `using Game.Core;`, not Game.UI"


def test_csharp_global_using_and_global_namespace(tmp_path: Path):
    gadget = _write(tmp_path / "gadget.cs", "public class Gadget {}\n")
    user = _write(tmp_path / "user.cs",
                  "global using System;\npublic class Widget : Gadget {}\n")
    result = extract([gadget, user], cache_root=tmp_path)

    g = _targets(result, "inherits", "Gadget")
    assert g, "expected an inherits edge to Gadget"
    assert all(x.get("source_file") for x in g), \
        "Widget : Gadget (both global namespace) must resolve; `global using` must not break parsing"


def test_csharp_cross_namespace_enum_reference_resolves_to_real_def(tmp_path: Path):
    core = _write(
        tmp_path / "core.cs",
        "namespace Game.Core { public enum Element { Fire, Ice } public class Damage {} }\n",
    )
    combat = _write(
        tmp_path / "combat.cs",
        "using Game.Core;\n"
        "namespace Game.Combat { public class Spell { Element element; Damage dmg; } }\n",
    )
    result = extract([core, combat], cache_root=tmp_path)

    element_defs = _defs(result, "Element")
    assert element_defs, "enum Element should be emitted as a real type definition node"
    assert all("core.cs" in n["source_file"] for n in element_defs)

    element_refs = [n for n in _targets(result, "references", "Element") if n.get("source_file")]
    assert element_refs, "Element field reference should resolve to the enum definition"
    assert all("core.cs" in n["source_file"] for n in element_refs)


def test_csharp_cross_namespace_struct_and_record_references_resolve(tmp_path: Path):
    core = _write(
        tmp_path / "core.cs",
        "namespace Game.Core { "
        "public struct Coord { public int X; } "
        "public record Player(string Name); "
        "}\n",
    )
    combat = _write(
        tmp_path / "combat.cs",
        "using Game.Core;\n"
        "namespace Game.Combat { public class Spell { Coord coord; Player player; } }\n",
    )
    result = extract([core, combat], cache_root=tmp_path)

    for label in ("Coord", "Player"):
        assert _defs(result, label), f"{label} should be emitted as a real type definition node"
        resolved = [n for n in _targets(result, "references", label) if n.get("source_file")]
        assert resolved, f"{label} field reference should resolve to the real definition"
        assert all("core.cs" in n["source_file"] for n in resolved)


def test_csharp_ambiguous_using_does_not_resolve(tmp_path: Path):
    # WeaponData is defined in BOTH Game.Core and Game.UI, and the referrer opens
    # BOTH namespaces. With two candidates the resolver must REFUSE (accept only a
    # unique hit) and leave the reference dangling on a shadow stub, rather than
    # fabricate an edge to an arbitrary, possibly-wrong definition.
    core = _write(
        tmp_path / "core.cs",
        "namespace Game.Core { public class WeaponData { public int Number; } }\n",
    )
    ui = _write(
        tmp_path / "ui.cs",
        "namespace Game.UI { public class WeaponData { public int Width; } }\n",
    )
    holder = _write(
        tmp_path / "holder.cs",
        "using Game.Core;\n"
        "using Game.UI;\n"
        "namespace Game.Combat { public class Holder { public WeaponData data; } }\n",
    )
    result = extract([core, ui, holder], cache_root=tmp_path)

    wd_refs = _targets(result, "references", "WeaponData")
    assert wd_refs, "expected a WeaponData reference edge (otherwise the test is vacuous)"
    resolved = [n for n in wd_refs if n.get("source_file")]
    assert not resolved, (
        "ambiguous WeaponData (Game.Core vs Game.UI, both opened) must NOT resolve to "
        f"either def; got wrong resolution(s): {[n.get('source_file') for n in resolved]}"
    )


def test_csharp_using_alias_resolves_to_aliased_type(tmp_path: Path):
    # `using Dmg = Game.Core.Damage;` is a single-type alias. A base type written as
    # `Dmg` has no other resolution route, so it must resolve to the real
    # Game.Core.Damage definition via the alias map -- not stay on a `Dmg` stub.
    core = _write(
        tmp_path / "core.cs",
        "namespace Game.Core { public class Damage {} }\n",
    )
    combat = _write(
        tmp_path / "combat.cs",
        "using Dmg = Game.Core.Damage;\n"
        "namespace Game.Combat { public class Weapon : Dmg {} }\n",
    )
    result = extract([core, combat], cache_root=tmp_path)

    damage = _targets(result, "inherits", "Damage")
    assert damage, "Weapon : Dmg must resolve (via the `using Dmg = ...` alias) to Damage"
    assert all("core.cs" in d["source_file"] for d in damage), (
        "the alias `Dmg` must resolve to the real Game.Core.Damage def, not a shadow stub"
    )


def test_csharp_namespace_nodes_canonical_and_discriminated(tmp_path: Path):
    a = _write(tmp_path / "a.cs", "namespace N { class A {} }\n")
    b = _write(tmp_path / "b.cs", "namespace N { class B {} }\n")
    nested = _write(tmp_path / "n.cs", "namespace Outer { namespace Inner { class C {} } }\n")
    result = extract([a, b, nested], cache_root=tmp_path)

    ns = [n for n in result["nodes"] if n.get("type") == "namespace"]
    by_label = {}
    for n in ns:
        by_label.setdefault(n["label"], []).append(n)
    assert len(by_label.get("N", [])) == 1, "namespace N must be one canonical node across files"
    assert "Outer.Inner" in by_label, sorted(by_label)
    assert all(n["id"].startswith("csharp_namespace:") for n in ns), [n["id"] for n in ns]


def test_csharp_import_edges_carry_using_kind(tmp_path: Path):
    f = _write(
        tmp_path / "a.cs",
        "using Game.Core;\nusing static System.Math;\nglobal using System;\n"
        "using X = Game.Core.Damage;\nclass Z {}\n",
    )
    result = extract([f], cache_root=tmp_path)
    imports = {
        (e["metadata"].get("using_kind"), e["metadata"].get("target_fqn"), e["metadata"].get("alias"))
        for e in result["edges"]
        if e.get("relation") == "imports" and e.get("metadata")
    }
    assert ("namespace", "Game.Core", None) in imports, imports
    assert ("namespace", "System", None) in imports, imports
    assert ("static", "System.Math", None) in imports, imports
    assert ("alias", "Game.Core.Damage", "X") in imports, imports


def test_csharp_import_edges_resolve_internal_namespace_and_alias(tmp_path: Path):
    core = _write(
        tmp_path / "core.cs",
        "namespace Game.Core { public class Damage {} }\n",
    )
    user = _write(
        tmp_path / "u.cs",
        "using Game.Core;\n"
        "using UnityEngine;\n"
        "using Dmg = Game.Core.Damage;\n"
        "using DMath = System.Math;\n"
        "using static Game.Core.Damage;\n"
        "class Z {}\n",
    )
    result = extract([core, user], cache_root=tmp_path)
    by_id = {n["id"]: n for n in result["nodes"]}
    imports = [
        (e["metadata"]["using_kind"], e["metadata"].get("target_fqn"), by_id.get(e["target"]))
        for e in result["edges"]
        if e.get("relation") == "imports" and (e.get("metadata") or {}).get("using_kind")
    ]

    assert ("namespace", "Game.Core", "namespace") in [
        (kind, fqn, target.get("type") if target else None)
        for kind, fqn, target in imports
    ]
    assert ("namespace", "UnityEngine", None) in [
        (kind, fqn, target.get("type") if target else None)
        for kind, fqn, target in imports
    ]
    assert ("alias", "Game.Core.Damage", "Damage") in [
        (kind, fqn, target.get("label") if target else None)
        for kind, fqn, target in imports
    ]
    assert ("alias", "System.Math", None) in [
        (kind, fqn, target.get("label") if target else None)
        for kind, fqn, target in imports
    ]
    assert ("static", "Game.Core.Damage", None) in [
        (kind, fqn, target.get("label") if target else None)
        for kind, fqn, target in imports
    ]
    assert not [
        n for n in result["nodes"]
        if not n.get("source_file") and n.get("label") in {"Game.Core", "Game.Core.Damage"}
    ]


def test_csharp_qualified_base_ref_is_flagged(tmp_path: Path):
    f = _write(tmp_path / "a.cs", "namespace N { class T {} class Use : B.T {} }\n")
    result = extract([f], cache_root=tmp_path)
    assert any((e.get("metadata") or {}).get("qualified") for e in result["edges"]), \
        "the qualified base ref B.T must carry metadata.qualified"


def test_csharp_one_file_same_name_no_collision_flag(tmp_path: Path):
    # ns_collision is gone: A.T and B.T are distinct nodes with no ns_collision metadata.
    dup = _write(tmp_path / "dup.cs", "namespace A { class T {} } namespace B { class T {} }\n")
    result = extract([dup], cache_root=tmp_path)
    tnodes = [n for n in result["nodes"] if n.get("label") == "T" and n.get("source_file")]
    assert len({n["id"] for n in tnodes}) == 2, tnodes
    assert not any((n.get("metadata") or {}).get("ns_collision") for n in tnodes), \
        "ns_collision must no longer be stamped"


def test_csharp_type_parameter_emits_no_reference(tmp_path: Path):
    f = _write(tmp_path / "a.cs", "namespace N { class T {} class Box<T> { T value; } }\n")
    result = extract([f], cache_root=tmp_path)
    real_t = {n["id"] for n in result["nodes"] if n.get("label") == "T" and n.get("source_file")}
    box_to_t = [
        e for e in result["edges"]
        if e.get("relation") in ("references", "inherits", "implements")
        and e.get("target") in real_t
        and "box" in str(e.get("source", "")).lower()
    ]
    assert not box_to_t, f"type parameter T must not produce a ref to the real N.T: {box_to_t}"


def test_csharp_nested_type_carries_metadata(tmp_path: Path):
    f = _write(tmp_path / "a.cs", "namespace N { class Outer { class Inner {} } }\n")
    result = extract([f], cache_root=tmp_path)
    inner = [n for n in result["nodes"] if n.get("label") == "Inner"]
    assert inner and inner[0].get("metadata", {}).get("is_nested_type") is True, inner


def test_csharp_cross_namespace_ref_not_misbound(tmp_path: Path):
    # Use in namespace B must NOT bind to C.T (B never opens C) - even though T is globally unique.
    f = _write(tmp_path / "x.cs", "namespace B { class Use : T {} } namespace C { class T {} }\n")
    result = extract([f], cache_root=tmp_path)
    resolved = [t for t in _targets(result, "inherits", "T") if t.get("source_file")]
    assert not resolved, f"Use:T in B must not bind C.T: {resolved}"


def test_csharp_same_file_cross_namespace_ref_not_misbound(tmp_path: Path):
    # Same file, T defined in B, Use in C : T - must NOT bind B.T (the eager same-file binding case).
    f = _write(tmp_path / "x.cs", "namespace B { class T {} } namespace C { class Use : T {} }\n")
    result = extract([f], cache_root=tmp_path)
    resolved = [t for t in _targets(result, "inherits", "T") if t.get("source_file")]
    assert not resolved, f"same-file Use:T in C must not bind B.T: {resolved}"


def test_csharp_inherits_does_not_bind_namespace_node(tmp_path: Path):
    # class Use : Game where Game is a namespace - must NOT bind the namespace node (Chunk-1 review B1).
    f = _write(tmp_path / "y.cs", "namespace Game { class Damage {} class Use : Game {} }\n")
    result = extract([f], cache_root=tmp_path)
    nsids = {n["id"] for n in result["nodes"] if n.get("type") == "namespace"}
    bad = [e for e in result["edges"] if e.get("relation") == "inherits" and e.get("target") in nsids]
    assert not bad, f"inherits must not target a namespace node: {bad}"


def test_csharp_qualified_ref_unknown_qualifier_dangles(tmp_path: Path):
    # B.T where B is neither a known namespace nor an alias -> must NOT bind A.T (sound dangle).
    f = _write(tmp_path / "a.cs", "namespace A { class T {} class Use : B.T {} }\n")
    result = extract([f], cache_root=tmp_path)
    resolved = [t for t in _targets(result, "inherits", "T") if t.get("source_file")]
    assert not resolved, f"unknown-qualifier B.T must not bind A.T: {resolved}"


def test_csharp_qualified_ref_known_namespace_resolves(tmp_path: Path):
    a = _write(tmp_path / "n.cs", "namespace N { class T {} }\n")
    b = _write(tmp_path / "m.cs", "namespace M { class Use : N.T {} }\n")
    result = extract([a, b], cache_root=tmp_path)
    n_t = next(n for n in result["nodes"] if n.get("label") == "T" and n.get("source_file"))
    use = next(n for n in result["nodes"] if n.get("label") == "Use")
    inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}
    assert (use["id"], n_t["id"]) in inh, "M.Use : N.T must bind N.T"


def test_csharp_qualified_generic_resolves_to_real_def(tmp_path: Path):
    # N.Box<int> previously emitted a junk 'B<C>'-style label; it must resolve to the real N.Box def.
    f = _write(tmp_path / "g.cs", "namespace N { class Box<TI> {} class Use { N.Box<int> b; } }\n")
    result = extract([f], cache_root=tmp_path)
    box = next(n for n in result["nodes"] if n.get("label") == "Box" and n.get("source_file"))
    use = next(n for n in result["nodes"] if n.get("label") == "Use")
    refs = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "references"}
    assert (use["id"], box["id"]) in refs, "N.Box<int> field must resolve to the real N.Box def"
    assert not any("<" in (n.get("label") or "") for n in result["nodes"]), \
        "no node should carry a junk generic label"


def test_csharp_qualified_alias_namespace_resolves(tmp_path: Path):
    # using B = X.Y (namespace alias) then B.T -> resolves the type T in namespace X.Y.
    a = _write(tmp_path / "n.cs", "namespace X.Y { class T {} }\n")
    b = _write(tmp_path / "m.cs", "using B = X.Y;\nnamespace M { class Use : B.T {} }\n")
    result = extract([a, b], cache_root=tmp_path)
    t = next(n for n in result["nodes"] if n.get("label") == "T" and n.get("source_file"))
    use = next(n for n in result["nodes"] if n.get("label") == "Use")
    inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}
    assert (use["id"], t["id"]) in inh, "B.T with `using B = X.Y;` must resolve to X.Y.T"


def test_csharp_qualified_out_of_scope_alias_falls_through_to_namespace(tmp_path: Path):
    # B is a real namespace AND an out-of-scope alias (declared in A, used in M):
    # B.T in M must resolve to namespace B's T, not dangle.
    a = _write(tmp_path / "b.cs", "namespace B { class T {} }\n")
    c = _write(tmp_path / "m.cs",
               "namespace A { using B = X.Y; }\nnamespace M { class Use : B.T {} }\n")
    result = extract([a, c], cache_root=tmp_path)
    b_t = next(n for n in result["nodes"] if n.get("label") == "T" and n.get("source_file"))
    use = next(n for n in result["nodes"] if n.get("label") == "Use")
    inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}
    assert (use["id"], b_t["id"]) in inh, "out-of-scope alias B must fall through to namespace B"


def test_csharp_qualified_in_scope_alias_shadows_namespace(tmp_path: Path):
    # B is both a real namespace AND an in-scope alias (B = X.Y) in A's block; a later out-of-scope
    # alias (B = Z.Q in C) must not overwrite it. Good : B.T -> X.Y.T, not namespace B's T.
    a = _write(tmp_path / "xy.cs", "namespace X.Y { class T {} }\n")
    b = _write(tmp_path / "b.cs", "namespace B { class T {} }\n")
    c = _write(tmp_path / "use.cs",
               "namespace A { using B = X.Y; class Good : B.T {} }\nnamespace C { using B = Z.Q; }\n")
    result = extract([a, b, c], cache_root=tmp_path)
    xy_t = next(n for n in result["nodes"]
                if n.get("label") == "T" and (n.get("metadata") or {}).get("namespace") == "X.Y")
    b_t = next(n for n in result["nodes"]
               if n.get("label") == "T" and (n.get("metadata") or {}).get("namespace") == "B")
    good = next(n for n in result["nodes"] if n.get("label") == "Good")
    inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}
    assert (good["id"], xy_t["id"]) in inh, "in-scope alias B=X.Y must resolve B.T to X.Y.T"
    assert (good["id"], b_t["id"]) not in inh, "must NOT bind namespace B's T"


def test_csharp_one_file_same_name_binds_own_namespace(tmp_path: Path):
    # T in both A and B of one file; Use:T in B must bind B.T (its own namespace), not A.T.
    f = _write(
        tmp_path / "c.cs",
        "namespace A { class T {} } namespace B { class T {} class Use : T {} }\n",
    )
    result = extract([f], cache_root=tmp_path)
    b_t = next(n for n in result["nodes"]
               if n.get("label") == "T" and (n.get("metadata") or {}).get("namespace") == "B")
    a_t = next(n for n in result["nodes"]
               if n.get("label") == "T" and (n.get("metadata") or {}).get("namespace") == "A")
    use = next(n for n in result["nodes"] if n.get("label") == "Use")
    inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}
    assert (use["id"], b_t["id"]) in inh, "Use:T in B must bind B.T"
    assert (use["id"], a_t["id"]) not in inh, "Use:T must NOT bind A.T"


def test_csharp_nested_type_not_importable_via_using(tmp_path: Path):
    # Inner is nested in Outer; `using N;` does not bring Inner into scope as a bare member.
    a = _write(tmp_path / "a.cs", "namespace N { class Outer { class Inner {} } }\n")
    b = _write(tmp_path / "b.cs", "using N;\nnamespace M { class Use { Inner x; } }\n")
    result = extract([a, b], cache_root=tmp_path)
    resolved = [t for t in _targets(result, "references", "Inner") if t.get("source_file")]
    assert not resolved, f"nested Inner must not resolve via `using N;`: {resolved}"


def test_csharp_generic_alias_resolves_to_base_type(tmp_path: Path):
    core = _write(tmp_path / "core.cs", "namespace N { class Box {} }\n")
    use = _write(tmp_path / "use.cs", "using Bx = N.Box<int>;\nclass Use : Bx {}\n")
    result = extract([core, use], cache_root=tmp_path)
    resolved = [t for t in _targets(result, "inherits", "Box") if t.get("source_file")]
    assert resolved, "generic alias `using Bx = N.Box<int>;` must resolve to the real Box def"


def test_csharp_type_ref_never_targets_a_file_label(tmp_path: Path):
    core = _write(tmp_path / "core.cs", "namespace N { class Box {} }\n")
    b = _write(tmp_path / "b.cs", "using B = N.Box;\nclass Use : B {}\n")
    result = extract([core, b], cache_root=tmp_path)
    bad = [
        e for e in result["edges"]
        if e.get("relation") in ("inherits", "implements", "references")
        and str(_node_by_id(result, e.get("target")).get("label", "") if _node_by_id(result, e.get("target")) else "").endswith(".cs")
    ]
    assert not bad, f"a C# type ref must not target a .cs file-labeled node: {bad}"


def test_csharp_type_ref_edges_carry_ref_token(tmp_path: Path):
    core = _write(tmp_path / "core.cs", "namespace N { class Base {} }\n")
    use = _write(tmp_path / "use.cs", "using N;\nnamespace M { class Use : Base {} }\n")
    result = extract([core, use], cache_root=tmp_path)
    inh = [
        e for e in result["edges"]
        if e.get("relation") == "inherits"
        and "use" in str(e.get("source", "")).lower()
    ]
    assert inh, "expected the Use : Base inherits edge"
    assert any((e.get("metadata") or {}).get("ref_token") == "Base" for e in inh), \
        "the inherits edge must carry metadata.ref_token == 'Base'"


def test_csharp_alias_matching_file_stem_resolves_via_token(tmp_path: Path):
    # alias name == file stem (B in b.cs) used to corrupt the target label; the
    # ref token makes the arbiter resolve it correctly regardless.
    core = _write(tmp_path / "core.cs", "namespace N { class Box {} }\n")
    b = _write(tmp_path / "b.cs", "using B = N.Box;\nclass Use : B {}\n")
    result = extract([core, b], cache_root=tmp_path)
    resolved = [t for t in _targets(result, "inherits", "Box") if t.get("source_file")]
    assert resolved, "Use : B (alias B == file stem) must resolve to the real Box def"


def test_csharp_same_name_diff_namespace_have_distinct_ids(tmp_path: Path):
    # The id now carries the namespace, so A.T and B.T are distinct nodes (resolution unchanged here).
    f = _write(tmp_path / "x.cs", "namespace A { class T {} } namespace B { class T {} }\n")
    result = extract([f], cache_root=tmp_path)
    ids = {n["id"] for n in result["nodes"] if n.get("label") == "T" and n.get("source_file")}
    assert len(ids) == 2, f"A.T and B.T must be distinct nodes: {ids}"


def test_csharp_global_scope_id_unchanged(tmp_path: Path):
    # A C# type at global scope (no namespace) keeps the bare stem+name id (empty namespace dropped by make_id).
    from graphify.extractors.base import _make_id, _file_stem
    f = _write(tmp_path / "g.cs", "class Glob {}\n")
    result = extract([f], cache_root=tmp_path)
    glob = next(n for n in result["nodes"] if n.get("label") == "Glob")
    stem = _file_stem(tmp_path / "g.cs")
    if "/" in stem:
        stem = stem.rsplit("/", 1)[-1]
    assert glob["id"] == _make_id(stem, "Glob"), glob
    assert "namespace" not in (glob.get("metadata") or {})


def test_csharp_namespaced_id_carries_namespace_segment(tmp_path: Path):
    f = _write(tmp_path / "n.cs", "namespace Game.Core { class Order {} }\n")
    result = extract([f], cache_root=tmp_path)
    order = next(n for n in result["nodes"] if n.get("label") == "Order")
    assert order["id"].endswith("order") and "game_core" in order["id"], order["id"]
    assert (order.get("metadata") or {}).get("namespace") == "Game.Core"

def test_csharp_two_namespaces_each_resolve_own_type(tmp_path: Path):
    f = _write(
        tmp_path / "two.cs",
        "namespace A { class T {} class UseA : T {} } namespace B { class T {} class UseB : T {} }\n",
    )
    result = extract([f], cache_root=tmp_path)

    def _n(label, ns):
        return next(x for x in result["nodes"]
                    if x.get("label") == label and (x.get("metadata") or {}).get("namespace") == ns)

    a_t, b_t, use_a, use_b = _n("T", "A"), _n("T", "B"), _n("UseA", "A"), _n("UseB", "B")
    inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}
    assert (use_a["id"], a_t["id"]) in inh and (use_b["id"], b_t["id"]) in inh
    assert (use_a["id"], b_t["id"]) not in inh and (use_b["id"], a_t["id"]) not in inh


def test_csharp_file_level_using_applies_across_blocks(tmp_path: Path):
    a = _write(tmp_path / "n.cs", "namespace N { class T {} }\n")
    b = _write(tmp_path / "u.cs", "using N;\nnamespace A { class X : T {} } namespace B { class Y : T {} }\n")
    result = extract([a, b], cache_root=tmp_path)
    resolved = [t["id"] for t in _targets(result, "inherits", "T") if t.get("source_file")]
    assert len(resolved) >= 2, f"file-level using N must reach both A.X and B.Y: {resolved}"


def test_csharp_namespace_scoped_using_isolated_to_sibling_block(tmp_path: Path):
    a = _write(tmp_path / "n.cs", "namespace N { class T {} }\n")
    b = _write(
        tmp_path / "u.cs",
        "namespace A { using N; class Good : T {} }\nnamespace A { class Bad : T {} }\n",
    )
    result = extract([a, b], cache_root=tmp_path)
    good = next(n for n in result["nodes"] if n.get("label") == "Good")
    bad = next(n for n in result["nodes"] if n.get("label") == "Bad")
    n_t = next(n for n in result["nodes"] if n.get("label") == "T" and n.get("source_file"))
    inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}
    assert (good["id"], n_t["id"]) in inh, "Good (same block as using N) must bind N.T"
    assert (bad["id"], n_t["id"]) not in inh, "Bad (sibling block, no using) must NOT bind N.T"


def test_csharp_using_flows_into_nested_block(tmp_path: Path):
    a = _write(tmp_path / "n.cs", "namespace N { class T {} }\n")
    b = _write(tmp_path / "u.cs", "namespace A { using N; namespace B { class Inner : T {} } }\n")
    result = extract([a, b], cache_root=tmp_path)
    resolved = [t["id"] for t in _targets(result, "inherits", "T") if t.get("source_file")]
    assert resolved, "using N in outer block A must flow into nested block B"


def test_csharp_alias_using_scoped_to_its_block(tmp_path: Path):
    a = _write(tmp_path / "n.cs", "namespace N { class T {} }\n")
    b = _write(
        tmp_path / "u.cs",
        "namespace A { using AliasT = N.T; class Good : AliasT {} }\nnamespace A { class Bad : AliasT {} }\n",
    )
    result = extract([a, b], cache_root=tmp_path)
    good = next(n for n in result["nodes"] if n.get("label") == "Good")
    bad = next(n for n in result["nodes"] if n.get("label") == "Bad")
    n_t = next(n for n in result["nodes"] if n.get("label") == "T" and n.get("source_file"))
    inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}
    assert (good["id"], n_t["id"]) in inh, "Good must bind N.T via the in-block alias"
    assert (bad["id"], n_t["id"]) not in inh, "Bad (sibling block) must NOT see the alias"


def test_csharp_same_file_enum_member_does_not_preempt_class_inheritance(tmp_path: Path):
    # Repro A (#3795): enum member Follow must not take the inheritance edge from class Follow.
    f = _write(
        tmp_path / "effects.cs",
        "namespace Demo {\n"
        "    public enum EffectKind { Tween, Follow }\n"
        "    public abstract class Follow { }\n"
        "    public sealed class FollowFloat : Follow { }\n"
        "}\n",
    )
    result = extract([f], cache_root=tmp_path)
    follow_class = next(
        n for n in result["nodes"]
        if n.get("label") == "Follow" and "effectkind" not in str(n.get("id", "")).lower()
    )
    follow_member = next(
        n for n in result["nodes"]
        if n.get("label") == "Follow" and "effectkind" in str(n.get("id", "")).lower()
    )
    followfloat = next(n for n in result["nodes"] if n.get("label") == "FollowFloat")
    inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}

    assert (followfloat["id"], follow_class["id"]) in inh, \
        "FollowFloat must inherit from class Follow"
    assert (followfloat["id"], follow_member["id"]) not in inh, \
        "FollowFloat must NOT inherit from enum member EffectKind.Follow"


def test_csharp_enum_member_cannot_steal_external_parameter_type(tmp_path: Path):
    # Repro B (#3795): Vector2 parameter must not bind to Channel.Vector2 enum member.
    kinds = _write(
        tmp_path / "Kinds.cs",
        "namespace Demo {\n"
        "    public enum Channel { Vector2 }\n"
        "}\n",
    )
    mover = _write(
        tmp_path / "Mover.cs",
        "using UnityEngine;\n"
        "namespace Demo {\n"
        "    public class Mover {\n"
        "        public void Move(Vector2 delta) { }\n"
        "    }\n"
        "}\n",
    )
    result = extract([kinds, mover], cache_root=tmp_path)
    channel_v2 = next(
        n for n in result["nodes"]
        if n.get("label") == "Vector2" and n.get("source_file")
    )
    move_refs = [
        e["target"] for e in result["edges"]
        if e.get("relation") == "references"
        and e.get("context") == "parameter_type"
        and "move" in str(e.get("source", "")).lower()
    ]
    assert channel_v2["id"] not in move_refs, \
        "Mover.Move(Vector2) must NOT resolve to Channel.Vector2 enum member"
    by_id = {n["id"]: n for n in result["nodes"]}
    assert any(
        by_id[tgt].get("label") == "Vector2" and not by_id[tgt].get("source_file")
        for tgt in move_refs
    ), f"Vector2 parameter must remain on an external stub: {move_refs}"


def test_csharp_property_cannot_steal_external_parameter_type(tmp_path: Path):
    # Repro B (#3795): System.Type parameter must not bind to Node.Type property.
    node = _write(
        tmp_path / "Node.cs",
        "namespace Demo {\n"
        "    public class Node {\n"
        "        public System.Type Type { get; set; }\n"
        "    }\n"
        "}\n",
    )
    mover = _write(
        tmp_path / "Mover.cs",
        "using System;\n"
        "namespace Demo {\n"
        "    public class Mover {\n"
        "        public string Describe(Type t) => t.Name;\n"
        "    }\n"
        "}\n",
    )
    result = extract([node, mover], cache_root=tmp_path)
    node_prop_type = next(
        n for n in result["nodes"]
        if n.get("label") == "Type" and n.get("source_file")
    )
    describe_refs = [
        e["target"] for e in result["edges"]
        if e.get("relation") == "references"
        and e.get("context") == "parameter_type"
        and "describe" in str(e.get("source", "")).lower()
    ]
    assert node_prop_type["id"] not in describe_refs, \
        "Mover.Describe(Type) must NOT resolve to Node.Type property"
    by_id = {n["id"]: n for n in result["nodes"]}
    assert any(
        by_id[tgt].get("label") == "Type" and not by_id[tgt].get("source_file")
        for tgt in describe_refs
    ), f"Type parameter must remain on an external stub: {describe_refs}"


def test_csharp_cross_file_collision_resolved_by_kind_not_filename_order(tmp_path: Path):
    # Enum member in A_Enum.cs (alphabetically first) must not steal inheritance from class in Follow.cs.
    a_enum = _write(
        tmp_path / "A_Enum.cs",
        "namespace Demo {\n"
        "    public enum EffectKind { Tween, Follow }\n"
        "}\n",
    )
    follow = _write(
        tmp_path / "Follow.cs",
        "namespace Demo {\n"
        "    public abstract class Follow { }\n"
        "    public sealed class FollowFloat : Follow { }\n"
        "}\n",
    )
    result = extract([a_enum, follow], cache_root=tmp_path)
    follow_class = next(
        n for n in result["nodes"]
        if n.get("label") == "Follow" and "follow.cs" in str(n.get("source_file", "")).lower()
    )
    follow_member = next(
        n for n in result["nodes"]
        if n.get("label") == "Follow" and "a_enum.cs" in str(n.get("source_file", "")).lower()
    )
    followfloat = next(n for n in result["nodes"] if n.get("label") == "FollowFloat")
    inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}

    assert (followfloat["id"], follow_class["id"]) in inh, \
        "FollowFloat must inherit from class Follow despite A_Enum.cs sorting first"
    assert (followfloat["id"], follow_member["id"]) not in inh, \
        "FollowFloat must NOT inherit from enum member EffectKind.Follow in A_Enum.cs"


def test_csharp_name_resolver_resolves_type_in_partial_graph_without_contains():
    # A partial or cross-repo graph intentionally lacks structural `contains` edges
    # from file nodes. CsharpNameResolver must still resolve valid types while
    # excluding member nodes (methods, properties, enum members).
    from graphify.extractors.csharp import CsharpNameResolver

    nodes = [
        {
            "id": "app::file",
            "label": "OrderService.cs",
            "source_file": "src/OrderService.cs",
            "file_type": "code",
            "repo": "app",
        },
        {
            "id": "app::run",
            "label": ".Run()",
            "source_file": "src/OrderService.cs",
            "file_type": "code",
            "repo": "app",
        },
        {
            "id": "lib::type",
            "label": "IValidator",
            "source_file": "src/IValidator.cs",
            "file_type": "code",
            "repo": "lib",
            "metadata": {"namespace": "Lib.Domain.Interfaces"},
        },
        {
            "id": "lib::method",
            "label": ".ValidateAsync()",
            "source_file": "src/IValidator.cs",
            "file_type": "code",
            "repo": "lib",
        },
    ]
    edges = [
        {
            "source": "app::file",
            "target": "app::using",
            "relation": "imports",
            "metadata": {"using_kind": "namespace", "target_fqn": "Lib.Domain.Interfaces"},
        },
        {
            "source": "lib::type",
            "target": "lib::method",
            "relation": "method",
        },
    ]

    resolver = CsharpNameResolver(nodes, edges)
    resolved, decisive = resolver.resolve_type_name(
        "IValidator",
        nodes[1],
        "src/OrderService.cs",
    )
    assert resolved == "lib::type", "Valid C# type must resolve even without `contains` edges"
    assert decisive is True


def _crlf_vs_lf(tmp_path: Path, files: dict[str, str]) -> tuple[dict, dict]:
    results = []
    for name, newline in (("lf", "\n"), ("crlf", "\r\n")):
        root = tmp_path / name
        paths = []
        for rel, text in files.items():
            p = root / rel
            p.parent.mkdir(parents=True, exist_ok=True)
            p.write_bytes(text.replace("\n", newline).encode("utf-8"))
            paths.append(p)
        results.append(extract(paths, root=root, cache_root=root))
    return results[0], results[1]


def test_csharp_scope_metadata_does_not_depend_on_line_endings(tmp_path: Path):
    """Namespace scope ids were byte offsets, so a CRLF checkout (Windows,
    core.autocrlf) and an LF checkout of the same commit wrote different
    `scope_chain` / `scope_id` metadata into graph.json. Row:column is the
    same for both."""
    files = {
        "core.cs": "// Core types.\nusing System;\n\nnamespace Game.Core\n{\n    public class Damage {}\n}\n",
        "combat.cs": (
            "// Combat.\n"
            "using System;\n"
            "\n"
            "namespace Game.Combat\n"
            "{\n"
            "    using Dmg = Game.Core.Damage;\n"
            "\n"
            "    public class Weapon : Dmg\n"
            "    {\n"
            "        public void Hit() {}\n"
            "    }\n"
            "}\n"
        ),
    }
    lf, crlf = _crlf_vs_lf(tmp_path, files)

    def shape(result: dict) -> tuple[list, list]:
        nodes = sorted(
            (n["id"], repr(sorted((n.get("metadata") or {}).items())), n.get("source_location"))
            for n in result["nodes"]
        )
        edges = sorted((e["source"], e["target"], e["relation"]) for e in result["edges"])
        return nodes, edges

    assert any("scope_chain" in (n.get("metadata") or {}) for n in lf["nodes"])
    assert shape(lf) == shape(crlf)
    damage = _targets(crlf, "inherits", "Damage")
    assert damage and all("core.cs" in d["source_file"] for d in damage), (
        "the namespace-scoped alias must still resolve in a CRLF file"
    )


def test_csharp_enclosing_namespace_dotted(tmp_path: Path):
    demo = _write(
        tmp_path / "Demo.cs",
        "namespace Demo {\n"
        "    public class Base {}\n"
        "    public class Widget {}\n"
        "}\n",
    )
    inner = _write(
        tmp_path / "Inner.cs",
        "namespace Demo.Inner {\n"
        "    public class Registry {\n"
        "        public Widget Item;\n"
        "    }\n"
        "    public class Factory : Base {\n"
        "        public object Make() {\n"
        "            return new Widget();\n"
        "        }\n"
        "    }\n"
        "}\n",
    )
    result = extract([demo, inner], cache_root=tmp_path)

    widget_defs = _defs(result, "Widget")
    base_defs = _defs(result, "Base")
    assert len(widget_defs) == 1 and "Demo.cs" in widget_defs[0]["source_file"]
    assert len(base_defs) == 1 and "Demo.cs" in base_defs[0]["source_file"]

    # Inherits: Factory : Base
    inherits_targets = [d for d in _targets(result, "inherits", "Base") if d.get("source_file")]
    assert inherits_targets and inherits_targets[0]["id"] == base_defs[0]["id"]

    # References: Registry.Item -> Widget
    ref_targets = [d for d in _targets(result, "references", "Widget") if d.get("source_file")]
    assert ref_targets and ref_targets[0]["id"] == widget_defs[0]["id"]

    # Calls: Factory.Make() -> new Widget()
    make_node = next(n for n in result["nodes"] if n.get("label") == ".Make()")
    calls = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "calls"}
    assert (make_node["id"], widget_defs[0]["id"]) in calls

    # No orphan shadow stubs
    shadow = [n for n in result["nodes"] if n.get("label") in ("Widget", "Base") and not n.get("source_file")]
    assert not shadow, f"unexpected shadow stubs: {[n['id'] for n in shadow]}"


def test_csharp_enclosing_namespace_nested_blocks(tmp_path: Path):
    source = _write(
        tmp_path / "nested.cs",
        "namespace Demo {\n"
        "    public class Base {}\n"
        "    public class Widget {}\n"
        "    namespace Inner {\n"
        "        public class Registry {\n"
        "            public Widget Item;\n"
        "        }\n"
        "        public class Factory : Base {\n"
        "            public object Make() {\n"
        "                return new Widget();\n"
        "            }\n"
        "        }\n"
        "    }\n"
        "}\n",
    )
    result = extract([source], cache_root=tmp_path)

    widget_def = _defs(result, "Widget")[0]
    base_def = _defs(result, "Base")[0]

    inherits_targets = [d for d in _targets(result, "inherits", "Base") if d.get("source_file")]
    assert inherits_targets and inherits_targets[0]["id"] == base_def["id"]

    ref_targets = [d for d in _targets(result, "references", "Widget") if d.get("source_file")]
    assert ref_targets and ref_targets[0]["id"] == widget_def["id"]

    make_node = next(n for n in result["nodes"] if n.get("label") == ".Make()")
    calls = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "calls"}
    assert (make_node["id"], widget_def["id"]) in calls

    shadow = [n for n in result["nodes"] if n.get("label") in ("Widget", "Base") and not n.get("source_file")]
    assert not shadow, f"unexpected shadow stubs: {[n['id'] for n in shadow]}"


def test_csharp_enclosing_namespace_file_scoped(tmp_path: Path):
    demo = _write(
        tmp_path / "Demo.cs",
        "namespace Demo;\n"
        "public class Base {}\n"
        "public class Widget {}\n",
    )
    scoped = _write(
        tmp_path / "Scoped.cs",
        "namespace Demo.Scoped;\n"
        "public class Registry {\n"
        "    public Widget Item;\n"
        "}\n"
        "public class Factory : Base {\n"
        "    public object Make() {\n"
        "        return new Widget();\n"
        "    }\n"
        "}\n",
    )
    result = extract([demo, scoped], cache_root=tmp_path)

    widget_def = _defs(result, "Widget")[0]
    base_def = _defs(result, "Base")[0]

    inherits_targets = [d for d in _targets(result, "inherits", "Base") if d.get("source_file")]
    assert inherits_targets and inherits_targets[0]["id"] == base_def["id"]

    ref_targets = [d for d in _targets(result, "references", "Widget") if d.get("source_file")]
    assert ref_targets and ref_targets[0]["id"] == widget_def["id"]

    make_node = next(n for n in result["nodes"] if n.get("label") == ".Make()")
    calls = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "calls"}
    assert (make_node["id"], widget_def["id"]) in calls

    shadow = [n for n in result["nodes"] if n.get("label") in ("Widget", "Base") and not n.get("source_file")]
    assert not shadow, f"unexpected shadow stubs: {[n['id'] for n in shadow]}"


def test_csharp_enclosing_namespace_multilevel_shadowing(tmp_path: Path):
    outer = _write(
        tmp_path / "outer.cs",
        "namespace Demo {\n"
        "    public class Widget { public int Level = 1; }\n"
        "}\n",
    )
    inner = _write(
        tmp_path / "inner.cs",
        "namespace Demo.Inner {\n"
        "    public class Widget { public int Level = 2; }\n"
        "}\n",
    )
    deep = _write(
        tmp_path / "deep.cs",
        "namespace Demo.Inner.Deep {\n"
        "    public class Consumer {\n"
        "        public Widget Item;\n"
        "    }\n"
        "}\n",
    )
    result = extract([outer, inner, deep], cache_root=tmp_path)

    inner_widget = next(
        n for n in _defs(result, "Widget")
        if "inner.cs" in n.get("source_file", "")
    )
    ref_targets = [d for d in _targets(result, "references", "Widget") if d.get("source_file")]
    assert ref_targets, "expected Widget reference to resolve"
    assert all(r["id"] == inner_widget["id"] for r in ref_targets), \
        "Demo.Inner.Deep must bind to Demo.Inner.Widget (innermost enclosing), not Demo.Widget"


def test_csharp_enclosing_namespace_regression_controls(tmp_path: Path):
    source = _write(
        tmp_path / "types.cs",
        "namespace Demo {\n"
        "    public class OuterType {}\n"
        "    public class CollidingType {}\n"
        "}\n"
        "namespace Other {\n"
        "    public class ImportedType {}\n"
        "}\n"
        "namespace Demo.Inner {\n"
        "    using Other;\n"
        "    public class CollidingType {}\n"
        "    public class Tester {\n"
        "        public CollidingType Local;\n"
        "        public ImportedType Imported;\n"
        "        public UnknownType Missing;\n"
        "    }\n"
        "}\n",
    )
    result = extract([source], cache_root=tmp_path)

    local_colliding = next(
        n for n in _defs(result, "CollidingType")
        if (n.get("metadata") or {}).get("namespace") == "Demo.Inner"
    )
    imported_type = _defs(result, "ImportedType")[0]

    # CollidingType reference must bind to Demo.Inner.CollidingType (current ns shadows enclosing)
    ref_colliding = [d for d in _targets(result, "references", "CollidingType") if d.get("source_file")]
    assert ref_colliding and all(r["id"] == local_colliding["id"] for r in ref_colliding)

    # ImportedType reference must bind to Other.ImportedType via using
    ref_imported = [d for d in _targets(result, "references", "ImportedType") if d.get("source_file")]
    assert ref_imported and all(r["id"] == imported_type["id"] for r in ref_imported)

    # UnknownType must remain a sourceless stub
    ref_missing = _targets(result, "references", "UnknownType")
    assert ref_missing and not any(r.get("source_file") for r in ref_missing)


# ── #4249: a generic and a non-generic type of the same name must not merge ──

def test_csharp_generic_and_non_generic_type_of_same_name_get_separate_nodes(tmp_path: Path):
    """`Effect` / `Effect<T>` (a common base-pair shape: `Comparer`/`Comparer<T>`
    is another) must not mint the same id, and the resulting inherits edge
    must not become a self-loop."""
    source = _write(
        tmp_path / "effects.cs",
        "namespace Demo {\n"
        "    public abstract class Effect { public virtual int ReadSlot(string p) { return 0; } }\n"
        "    public abstract class Effect<T> : Effect where T : unmanaged { }\n"
        "    public abstract class Tween<T> : Effect<T> where T : unmanaged {\n"
        "        public override int ReadSlot(string p) { return base.ReadSlot(p) + 1; }\n"
        "    }\n"
        "}\n",
    )
    result = extract([source], cache_root=tmp_path)

    effects = _defs(result, "Effect")
    assert len(effects) == 2, f"generic and non-generic Effect must be separate nodes: {effects}"

    inherits = {(e["source"], e["target"]) for e in result["edges"] if e["relation"] == "inherits"}
    assert not any(src == tgt for src, tgt in inherits), (
        f"a generic type inheriting its own non-generic base must never be a self-loop: {inherits}"
    )

    tween = _defs(result, "Tween")[0]
    effect_targets = {tgt for src, tgt in inherits if src == tween["id"]}
    assert effect_targets, "Tween<T> must have an inherits edge"
    assert effect_targets <= {e["id"] for e in effects}, (
        "Tween<T> must inherit one of the two real Effect definitions, not a dangling stub"
    )


def test_csharp_non_generic_pair_with_no_relation_stays_two_plain_nodes(tmp_path: Path):
    """Control: two same-named non-generic types in different namespaces are
    already correctly separate — confirms the fix does not touch that case."""
    source = _write(
        tmp_path / "pair.cs",
        "namespace A { public class Worker {} }\n"
        "namespace B { public class Worker {} }\n",
    )
    result = extract([source], cache_root=tmp_path)
    workers = _defs(result, "Worker")
    assert len(workers) == 2
    assert workers[0]["id"] != workers[1]["id"]
# -------------------------------------------------------------------------
# Issue #4216: C#: nested types imported with `using static` are resolved
# -------------------------------------------------------------------------


def test_csharp_using_static_resolves_nested_type_reference(tmp_path: Path):
    layer = _write(
        tmp_path / "layer.cs",
        "namespace Demo\n{\n    public class Layer\n    {\n        public class Record {}\n    }\n}\n",
    )
    consumer = _write(
        tmp_path / "consumer.cs",
        "using static Demo.Layer;\nnamespace Consumer\n{\n    public class Service\n    {\n        public Record value;\n    }\n}\n",
    )
    result = extract([layer, consumer], cache_root=tmp_path)
    record_defs = _defs(result, "Record")
    assert len(record_defs) == 1, "nested Record definition must exist"
    record_def = record_defs[0]

    resolved = [t for t in _targets(result, "references", "Record") if t.get("source_file")]
    assert resolved, "Record reference must resolve to the real definition via `using static Demo.Layer;`"
    assert resolved[0]["id"] == record_def["id"]


def test_csharp_using_static_resolves_nested_constructor_call(tmp_path: Path):
    layer = _write(
        tmp_path / "layer.cs",
        "namespace Demo\n{\n    public class Layer\n    {\n        public class Record {}\n    }\n}\n",
    )
    consumer = _write(
        tmp_path / "consumer.cs",
        (
            "using static Demo.Layer;\n"
            "namespace Consumer\n"
            "{\n"
            "    public class Service\n"
            "    {\n"
            "        public void Run()\n"
            "        {\n"
            "            var r = new Record();\n"
            "        }\n"
            "    }\n"
            "}\n"
        ),
    )
    result = extract([layer, consumer], cache_root=tmp_path)
    record_defs = _defs(result, "Record")
    assert len(record_defs) == 1, "nested Record definition must exist"
    record_def = record_defs[0]

    resolved_calls = [t for t in _targets(result, "calls", "Record") if t.get("source_file")]
    assert resolved_calls, "new Record() call must resolve to nested Record via `using static Demo.Layer;`"
    assert resolved_calls[0]["id"] == record_def["id"]


def test_csharp_using_static_multi_level_nested_type(tmp_path: Path):
    print_file = _write(
        tmp_path / "print.cs",
        (
            "namespace Demo\n"
            "{\n"
            "    public class Print\n"
            "    {\n"
            "        public class Engine\n"
            "        {\n"
            "            public class Info {}\n"
            "        }\n"
            "    }\n"
            "}\n"
        ),
    )
    consumer = _write(
        tmp_path / "consumer.cs",
        "using static Demo.Print.Engine;\nnamespace Consumer\n{\n    public class App\n    {\n        public Info value;\n    }\n}\n",
    )
    result = extract([print_file, consumer], cache_root=tmp_path)
    info_defs = _defs(result, "Info")
    assert len(info_defs) == 1, "nested Info definition must exist"
    info_def = info_defs[0]

    resolved = [t for t in _targets(result, "references", "Info") if t.get("source_file")]
    assert resolved, "Info reference must resolve via multi-level `using static Demo.Print.Engine;`"
    assert resolved[0]["id"] == info_def["id"]


def test_csharp_using_static_on_partial_holder_class(tmp_path: Path):
    slice_a = _write(
        tmp_path / "layer_a.cs",
        (
            "namespace Demo\n"
            "{\n"
            "    public partial class Layer\n"
            "    {\n"
            "        public class SliceA {}\n"
            "        public partial class SharedNested {}\n"
            "    }\n"
            "}\n"
        ),
    )
    slice_b = _write(
        tmp_path / "layer_b.cs",
        (
            "namespace Demo\n"
            "{\n"
            "    public partial class Layer\n"
            "    {\n"
            "        public class SliceB {}\n"
            "        public partial class SharedNested {}\n"
            "    }\n"
            "}\n"
        ),
    )
    consumer = _write(
        tmp_path / "consumer.cs",
        (
            "using static Demo.Layer;\n"
            "namespace Consumer\n"
            "{\n"
            "    public class App\n"
            "    {\n"
            "        public SliceA a;\n"
            "        public SliceB b;\n"
            "        public SharedNested s;\n"
            "    }\n"
            "}\n"
        ),
    )
    result = extract([slice_a, slice_b, consumer], cache_root=tmp_path)

    layer_defs = _defs(result, "Layer")
    assert len(layer_defs) == 1, "top-level partial Layer should merge into one canonical node"
    layer_nid = layer_defs[0]["id"]

    contains_targets = {
        e["target"] for e in result["edges"]
        if e.get("relation") == "contains" and e.get("source") == layer_nid
    }
    slice_a_defs = _defs(result, "SliceA")
    slice_b_defs = _defs(result, "SliceB")
    shared_defs = _defs(result, "SharedNested")

    assert len(slice_a_defs) == 1
    assert len(slice_b_defs) == 1
    assert len(shared_defs) == 1, "nested partial SharedNested halves merge under the same outer"
    assert shared_defs[0]["id"] in contains_targets
    assert slice_a_defs[0]["id"] in contains_targets
    assert slice_b_defs[0]["id"] in contains_targets

    resolved_a = [t for t in _targets(result, "references", "SliceA") if t.get("source_file")]
    assert resolved_a and resolved_a[0]["id"] == slice_a_defs[0]["id"]

    resolved_b = [t for t in _targets(result, "references", "SliceB") if t.get("source_file")]
    assert resolved_b and resolved_b[0]["id"] == slice_b_defs[0]["id"]

    resolved_s = [t for t in _targets(result, "references", "SharedNested") if t.get("source_file")]
    assert resolved_s, "SharedNested reference must resolve via `using static Demo.Layer;`"
    assert resolved_s[0]["id"] in {d["id"] for d in shared_defs}


def test_csharp_using_static_scoped_to_namespace_block(tmp_path: Path):
    layer = _write(
        tmp_path / "layer.cs",
        "namespace Demo\n{\n    public class Layer\n    {\n        public class Record {}\n    }\n}\n",
    )
    consumer = _write(
        tmp_path / "consumer.cs",
        (
            "namespace Good\n"
            "{\n"
            "    using static Demo.Layer;\n"
            "    public class Service\n"
            "    {\n"
            "        public Record goodValue;\n"
            "    }\n"
            "}\n"
            "namespace Bad\n"
            "{\n"
            "    public class OtherService\n"
            "    {\n"
            "        public Record badValue;\n"
            "    }\n"
            "}\n"
        ),
    )
    result = extract([layer, consumer], cache_root=tmp_path)
    record_defs = _defs(result, "Record")
    assert len(record_defs) == 1
    record_def = record_defs[0]

    found_good = False
    found_bad = False
    for e in result["edges"]:
        if e.get("relation") != "references":
            continue
        tgt = _node_by_id(result, e.get("target", ""))
        if tgt and tgt.get("label") == "Record":
            src = _node_by_id(result, e.get("source", ""))
            src_label = src.get("label", "") if src else ""
            if src_label == "Service":
                found_good = True
                assert tgt.get("source_file"), "Good.Service must resolve Record"
                assert tgt["id"] == record_def["id"]
            elif src_label == "OtherService":
                found_bad = True
                assert not tgt.get("source_file"), "Bad.OtherService must NOT resolve Record"

    assert found_good, "Good.Service Record reference edge must be found"
    assert found_bad, "Bad.OtherService Record reference edge must be found"


def test_csharp_unimported_nested_type_dangles(tmp_path: Path):
    layer = _write(
        tmp_path / "layer.cs",
        "namespace Demo\n{\n    public class Layer\n    {\n        public class Record {}\n    }\n}\n",
    )
    consumer = _write(
        tmp_path / "consumer.cs",
        "namespace Consumer\n{\n    public class Service\n    {\n        public Record value;\n    }\n}\n",
    )
    result = extract([layer, consumer], cache_root=tmp_path)
    resolved = [t for t in _targets(result, "references", "Record") if t.get("source_file")]
    assert not resolved, f"unimported nested Record must not resolve: {resolved}"


def test_csharp_ambiguous_static_imports_dangle(tmp_path: Path):
    layer_a = _write(
        tmp_path / "layer_a.cs",
        "namespace DemoA\n{\n    public class LayerA\n    {\n        public class Record {}\n    }\n}\n",
    )
    layer_b = _write(
        tmp_path / "layer_b.cs",
        "namespace DemoB\n{\n    public class LayerB\n    {\n        public class Record {}\n    }\n}\n",
    )
    consumer = _write(
        tmp_path / "consumer.cs",
        (
            "using static DemoA.LayerA;\n"
            "using static DemoB.LayerB;\n"
            "namespace Consumer\n"
            "{\n"
            "    public class Service\n"
            "    {\n"
            "        public Record value;\n"
            "    }\n"
            "}\n"
        ),
    )
    result = extract([layer_a, layer_b, consumer], cache_root=tmp_path)
    resolved = [t for t in _targets(result, "references", "Record") if t.get("source_file")]
    assert not resolved, f"ambiguous static imports must fail closed and remain unresolved: {resolved}"


def test_csharp_static_import_vs_namespace_conflict_dangles(tmp_path: Path):
    other = _write(
        tmp_path / "other.cs",
        "namespace Other\n{\n    public class Record {}\n}\n",
    )
    layer = _write(
        tmp_path / "layer.cs",
        "namespace Demo\n{\n    public class Layer\n    {\n        public class Record {}\n    }\n}\n",
    )
    consumer = _write(
        tmp_path / "consumer.cs",
        (
            "using Other;\n"
            "using static Demo.Layer;\n"
            "namespace Consumer\n"
            "{\n"
            "    public class Service\n"
            "    {\n"
            "        public Record value;\n"
            "    }\n"
            "}\n"
        ),
    )
    result = extract([other, layer, consumer], cache_root=tmp_path)
    resolved = [t for t in _targets(result, "references", "Record") if t.get("source_file")]
    assert not resolved, f"conflict between namespace and static import must fail closed: {resolved}"


def test_csharp_external_type_shares_one_placeholder_across_files(tmp_path: Path):
    # #4247: an external type referenced from several files must get ONE sourceless
    # placeholder keyed by its name, shared by every file -- not a separate
    # per-file placeholder (`a_cs_list`, `b_cs_list`) that leaves the files
    # unconnected.
    a = _write(
        tmp_path / "A.cs",
        "using System.Collections.Generic;\n"
        "namespace Demo { public class A { public List<int> Items; } }\n",
    )
    b = _write(
        tmp_path / "B.cs",
        "using System.Collections.Generic;\n"
        "namespace Demo { public class B { public List<string> Names; } }\n",
    )
    result = extract([a, b], cache_root=tmp_path)

    list_stubs = [
        n for n in result["nodes"]
        if n.get("label") == "List" and not n.get("source_file")
    ]
    assert len(list_stubs) == 1, (
        f"expected one shared sourceless List placeholder, got {len(list_stubs)}: "
        f"{[n.get('id') for n in list_stubs]}"
    )
    stub_id = list_stubs[0]["id"]

    referring_files = {
        e.get("source_file") for e in result["edges"]
        if e.get("relation") == "references" and e.get("target") == stub_id
    }
    # Both A.cs and B.cs reach the one shared placeholder, so the files connect.
    assert len({str(f).replace("\\", "/").rsplit("/", 1)[-1] for f in referring_files}) == 2, (
        f"both files must reference the shared List placeholder: {referring_files}"
    )
    # No per-file placeholder survives alongside the canonical one.
    assert not any(
        n.get("label") == "List" and not n.get("source_file") and n.get("id") != stub_id
        for n in result["nodes"]
    )