"""C# receiver-typed member-call resolution (#1609).

`recv.Method()` where `recv` is a typed field / property / parameter / local must
resolve to the receiver TYPE's method — not a bare same-named match. Before this,
C# had no member-call resolver: the bare method name matched any same-named method
in the corpus, so `_server.Save()` silently mis-bound to an unrelated `Cache.Save()`
(a WRONG edge, not just a missing one). Resolution is by receiver type with the
single-definition god-node guard; an untypable receiver produces no edge.
"""
from __future__ import annotations

import os
from pathlib import Path

from graphify.extract import extract


def _calls(tmp_path, files: dict[str, str]):
    for name, body in files.items():
        p = tmp_path / name
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(body)
    old = os.getcwd()
    try:
        os.chdir(tmp_path)
        r = extract([Path(n) for n in files], cache_root=tmp_path / ".cache")
    finally:
        os.chdir(old)
    calls = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "calls"}
    return calls, r


_AMBIG = {
    "S.cs": (
        "public class Server { public bool Save() => true; }\n"
        "public class Cache  { public bool Save() => false; }\n"
        "public class Repo {\n"
        "    private Server _server = new Server();\n"
        "    public bool Commit() { return _server.Save(); }\n"
        "}\n"
    )
}


def _find(r, label, id_contains):
    return next(n["id"] for n in r["nodes"]
               if n["label"] == label and id_contains in n["id"])


def test_field_receiver_resolves_to_declared_type_not_bare_match(tmp_path):
    calls, r = _calls(tmp_path, _AMBIG)
    commit = _find(r, ".Commit()", "commit")
    server_save = _find(r, ".Save()", "server")
    cache_save = _find(r, ".Save()", "cache")
    assert (commit, server_save) in calls, "field.Method() must resolve to the field's type"
    assert (commit, cache_save) not in calls, "must NOT mis-bind to an unrelated same-named method"


def test_parameter_receiver_resolves(tmp_path):
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class Server { public bool Save() => true; }\n"
            "public class Cache  { public bool Save() => false; }\n"
            "public class Svc { public static bool Copy(Server server) { return server.Save(); } }\n"
        )
    })
    assert any("copy" in s and "server_save" in t for s, t in calls)
    assert not any("copy" in s and "cache_save" in t for s, t in calls)


def test_local_var_receiver_resolves(tmp_path):
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class Server { public bool Save() => true; }\n"
            "public class R {\n"
            "    public bool A() { Server s = new Server(); return s.Save(); }\n"
            "    public bool B() { var v = new Server(); return v.Save(); }\n"
            "}\n"
        )
    })
    assert any("_r_a" in s and "server_save" in t for s, t in calls), "explicit-typed local"
    assert any("_r_b" in s and "server_save" in t for s, t in calls), "var = new T() local"


def test_cross_file_receiver_resolves(tmp_path):
    calls, r = _calls(tmp_path, {
        "Server.cs": (
            "public class Server { public bool Save() => true; }\n"
            "public class Cache  { public bool Save() => false; }\n"
        ),
        "Repo.cs": (
            "public class Repo { private Server _s = new Server(); "
            "public bool Commit() { return _s.Save(); } }\n"
        ),
    })
    assert any("commit" in s and "server_save" in t for s, t in calls)
    assert not any("commit" in s and "cache_save" in t for s, t in calls)


def test_this_and_static_receivers(tmp_path):
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class Util { public static int F() => 1; }\n"
            "public class R {\n"
            "    public bool A() { return this.B(); }\n"
            "    public bool B() => true;\n"
            "    public int G() { return Util.F(); }\n"
            "}\n"
        )
    })
    assert any("_r_a" in s and "_r_b" in t for s, t in calls), "this.B() -> R.B"
    assert any("_r_g" in s and "util_f" in t for s, t in calls), "Util.F() -> Util.F"


def test_untyped_receiver_emits_no_edge(tmp_path):
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class Server { public bool Save() => true; }\n"
            "public class R { public bool C(dynamic x) { return x.Save(); } }\n"
        )
    })
    assert not any("save" in t.lower() for _s, t in calls), "dynamic receiver must not resolve"


def test_method_absent_on_type_emits_no_edge(tmp_path):
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class Server { public bool Save() => true; }\n"
            "public class R { private Server _s = new Server(); "
            "public bool C() { return _s.Missing(); } }\n"
        )
    })
    assert not any("_r_c" in s and "save" in t.lower() for s, t in calls)


def test_unqualified_call_still_resolves(tmp_path):
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class R { public bool A() { Helper(); return true; } "
            "private void Helper() {} }\n"
        )
    })
    assert any("_r_a" in s and "helper" in t for s, t in calls), "no regression on unqualified calls"


# ── Namespace-aware receiver typing + shadow poisoning (#1620) ────────────────

_NS_AB = {
    "A.cs": "namespace A { public class Svc { public bool Do() => true; } }\n",
    "B.cs": "namespace B { public class Svc { public bool Do() => false; } }\n",
}


def test_namespace_using_directive_disambiguates_receiver_type(tmp_path):
    """`Svc` exists in namespaces A and B; a caller file `using A;` must bind an
    `A.Svc`-typed receiver to A.Svc.Do — before #1620 the corpus-wide bare-name
    ambiguity made the resolver bail (missing edge)."""
    calls, r = _calls(tmp_path, {
        **_NS_AB,
        "Caller.cs": (
            "using A;\n"
            "namespace App {\n"
            "    public class Runner { public bool Go(Svc s) { return s.Do(); } }\n"
            "}\n"
        ),
    })
    a_do = _find(r, ".Do()", "a_a_svc")
    b_do = _find(r, ".Do()", "b_b_svc")
    runner_go = _find(r, ".Go()", "runner")
    assert (runner_go, a_do) in calls, "using A; must resolve Svc to A.Svc"
    assert (runner_go, b_do) not in calls, "must NOT bind to the same-named B.Svc"


def test_namespace_using_directive_resolves_to_other_namespace(tmp_path):
    calls, r = _calls(tmp_path, {
        **_NS_AB,
        "Caller.cs": (
            "using B;\n"
            "namespace App {\n"
            "    public class Runner { public bool Go(Svc s) { return s.Do(); } }\n"
            "}\n"
        ),
    })
    a_do = _find(r, ".Do()", "a_a_svc")
    b_do = _find(r, ".Do()", "b_b_svc")
    runner_go = _find(r, ".Go()", "runner")
    assert (runner_go, b_do) in calls, "using B; must resolve Svc to B.Svc"
    assert (runner_go, a_do) not in calls


def test_namespace_ambiguous_without_using_bails(tmp_path):
    """No using directive and `Svc` in two foreign namespaces: genuinely
    ambiguous — no edge to either candidate (never a guess)."""
    calls, r = _calls(tmp_path, {
        **_NS_AB,
        "Caller.cs": (
            "namespace App {\n"
            "    public class Runner { public bool Go(Svc s) { return s.Do(); } }\n"
            "}\n"
        ),
    })
    assert not any("runner" in s and "svc_do" in t for s, t in calls), \
        "ambiguous cross-namespace type must produce no edge"


def test_same_namespace_receiver_resolves_without_using(tmp_path):
    """A caller in namespace A resolves `Svc` to A.Svc even though B.Svc also
    exists — same-namespace visibility needs no using directive."""
    calls, r = _calls(tmp_path, {
        **_NS_AB,
        "A2.cs": (
            "namespace A {\n"
            "    public class Client { public bool Go(Svc s) { return s.Do(); } }\n"
            "}\n"
        ),
    })
    a_do = _find(r, ".Do()", "a_a_svc")
    b_do = _find(r, ".Do()", "b_b_svc")
    client_go = _find(r, ".Go()", "client")
    assert (client_go, a_do) in calls
    assert (client_go, b_do) not in calls


def test_local_shadowing_field_of_different_type_poisons_name(tmp_path):
    """A local `Other x` shadowing a field `Server x` makes the name's type
    conflicting — the binding is poisoned and `x.Run()` emits NO edge, instead
    of first-binding-wins mis-binding to Server.Run (a wrong edge)."""
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class Server { public bool Run() => true; }\n"
            "public class Other  { public bool Run() => false; }\n"
            "public class Holder {\n"
            "    private Server x = new Server();\n"
            "    public bool A() { Other x = new Other(); return x.Run(); }\n"
            "}\n"
        )
    })
    holder_a = _find(r, ".A()", "holder")
    server_run = _find(r, ".Run()", "server")
    other_run = _find(r, ".Run()", "other")
    assert (holder_a, server_run) not in calls, \
        "shadowed field's type must not win (wrong edge)"
    assert (holder_a, other_run) not in calls, \
        "conflicting bindings poison the name entirely (conservative: no edge)"


def test_untyped_redeclaration_poisons_typed_field(tmp_path):
    """`var x = Compute();` (untypable) redeclaring a typed field poisons the
    name: `x.Run()` must not bind to the field's type."""
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class Server { public bool Run() => true; }\n"
            "public class Holder {\n"
            "    private Server x = new Server();\n"
            "    public object Compute() => new object();\n"
            "    public bool A() { var x = Compute(); return x.Run(); }\n"
            "}\n"
        )
    })
    assert not any("holder_a" in s and "run" in t.lower() for s, t in calls)


def test_this_field_receiver_resolves(tmp_path):
    """`this._s.Save()` types the field exactly like a bare `_s.Save()`."""
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class Server { public bool Save() => true; }\n"
            "public class Cache  { public bool Save() => false; }\n"
            "public class Repo {\n"
            "    private Server _s = new Server();\n"
            "    public bool Commit() { return this._s.Save(); }\n"
            "}\n"
        )
    })
    commit = _find(r, ".Commit()", "commit")
    server_save = _find(r, ".Save()", "server")
    cache_save = _find(r, ".Save()", "cache")
    assert (commit, server_save) in calls
    assert (commit, cache_save) not in calls


def test_base_receiver_resolves_to_base_class_method(tmp_path):
    calls, r = _calls(tmp_path, {
        "Base.cs": "public class BaseSvc { public bool Ping() => true; }\n",
        "Sub.cs": (
            "public class Sub : BaseSvc {\n"
            "    public bool Go() { return base.Ping(); }\n"
            "}\n"
        ),
    })
    sub_go = _find(r, ".Go()", "sub")
    ping = _find(r, ".Ping()", "basesvc")
    assert (sub_go, ping) in calls, "base.Ping() must resolve to the base class method"


def test_inherited_method_resolves_through_base_chain(tmp_path):
    """A method not declared on the receiver's type but inherited from a
    resolvable in-corpus base resolves to the base's declaration."""
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class BaseSvc { public bool Ping() => true; }\n"
            "public class Derived : BaseSvc { }\n"
            "public class User {\n"
            "    public bool Use(Derived d) { return d.Ping(); }\n"
            "}\n"
        )
    })
    use = _find(r, ".Use()", "user")
    ping = _find(r, ".Ping()", "basesvc")
    assert (use, ping) in calls


def test_unresolved_base_poisons_inherited_member_lookup(tmp_path):
    """The receiver's type inherits from an out-of-corpus base: a method missing
    on the type may live on that base, so the lookup is poisoned — and it must
    NOT fall back to an unrelated same-named in-corpus method."""
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class Server { public bool Save() => true; }\n"
            "public class Ext : NotInCorpus { }\n"
            "public class User {\n"
            "    public bool U(Ext e) { return e.Save(); }\n"
            "}\n"
        )
    })
    assert not any("user_u" in s and "save" in t.lower() for s, t in calls), \
        "unresolved base chain must bail, not mis-bind to Server.Save"


# ── Method-scoped receiver typing (#2299) ────────────────────────────────────
# C# scoping is per-method: a name rebound (even untypably) in ONE method must
# not poison a same-named, explicitly typed receiver in a DIFFERENT method. The
# old file-wide table did exactly that, silently deleting true calls edges.


def test_cross_method_name_reuse_does_not_poison(tmp_path):
    """#2299 corpus: `var item = items[i]` (untypable) in RunIndexed must not
    poison the explicitly typed `Item item` parameter in RunOne."""
    calls, r = _calls(tmp_path, {
        "Item.cs": (
            "namespace Demo {\n"
            "    public class Item { public void Handle() {} }\n"
            "}\n"
        ),
        "Runner.cs": (
            "using System.Collections.Generic;\n"
            "namespace Demo {\n"
            "    public class Runner {\n"
            "        public void RunOne(Item item) { item.Handle(); }\n"
            "        public void RunIndexed(List<Item> items, int i) {\n"
            "            var item = items[i];\n"
            "            item.Handle();\n"
            "        }\n"
            "    }\n"
            "}\n"
        ),
    })
    run_one = _find(r, ".RunOne()", "runner")
    run_indexed = _find(r, ".RunIndexed()", "runner")
    handle = _find(r, ".Handle()", "item")
    assert (run_one, handle) in calls, \
        "typed param receiver must resolve despite a same-named untypable local elsewhere"
    edge = next(e for e in r["edges"] if e["relation"] == "calls"
                and e["source"] == run_one and e["target"] == handle)
    assert edge["confidence"] == "INFERRED"
    assert (run_indexed, handle) not in calls, \
        "the untypable local (`var item = items[i]`) stays unresolved — no guessed edge"


def test_per_method_locals_resolve_independently(tmp_path):
    """Same local name bound to DIFFERENT types in different methods: each
    method resolves to its own binding (the file-wide table poisoned both)."""
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class HtmlWriter { public void Render() {} }\n"
            "public class TextWriter { public void Render() {} }\n"
            "public class Doc {\n"
            "    public void AsHtml() { var w = new HtmlWriter(); w.Render(); }\n"
            "    public void AsText() { var w = new TextWriter(); w.Render(); }\n"
            "}\n"
        )
    })
    as_html = _find(r, ".AsHtml()", "doc")
    as_text = _find(r, ".AsText()", "doc")
    html_render = _find(r, ".Render()", "htmlwriter")
    text_render = _find(r, ".Render()", "textwriter")
    assert (as_html, html_render) in calls
    assert (as_text, text_render) in calls
    assert (as_html, text_render) not in calls, "no cross-method binding leak"
    assert (as_text, html_render) not in calls, "no cross-method binding leak"


def test_same_method_shadow_still_poisons(tmp_path):
    """Keep-the-bar: a SAME-method conflict (param `Server x` + local `Other x`)
    still poisons the name — raw calls carry no lexical position, so neither
    candidate may win."""
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class Server { public bool Run() => true; }\n"
            "public class Other  { public bool Run() => false; }\n"
            "public class Holder {\n"
            "    public bool A(Server x) { Other x = new Other(); return x.Run(); }\n"
            "}\n"
        )
    })
    holder_a = _find(r, ".A()", "holder")
    server_run = _find(r, ".Run()", "server")
    other_run = _find(r, ".Run()", "other")
    assert (holder_a, server_run) not in calls
    assert (holder_a, other_run) not in calls


def test_file_scoped_namespace_receiver_resolves(tmp_path):
    """The C# 10 file-scoped namespace form (`namespace Demo;`) types receivers
    the same as the braced form."""
    calls, r = _calls(tmp_path, {
        "Item.cs": (
            "namespace Demo;\n"
            "public class Item { public void Handle() {} }\n"
        ),
        "Runner.cs": (
            "namespace Demo;\n"
            "public class Runner {\n"
            "    public void RunOne(Item item) { item.Handle(); }\n"
            "}\n"
        ),
    })
    run_one = _find(r, ".RunOne()", "runner")
    handle = _find(r, ".Handle()", "item")
    assert (run_one, handle) in calls


def test_method_chained_off_new_expression_resolves(tmp_path):
    """#1770: a method invoked directly on a `new X(...)` object-creation
    expression (no intermediate variable) must still emit a calls edge to the
    constructed type's method — the fluent `new X(...).M()` pattern."""
    calls, r = _calls(tmp_path, {
        "S.cs": (
            "public class Merger {\n"
            "    public Merger(int x) {}\n"
            "    public int Combine(int a, bool b) { return a; }\n"
            "}\n"
            "public class Svc {\n"
            "    public int Run(int ctx) {\n"
            "        return new Merger(ctx).Combine(ctx, true);\n"
            "    }\n"
            "}\n"
        )
    })
    label = {n["id"]: n.get("label") for n in r["nodes"]}
    assert any(
        "run" in s and label.get(t) == ".Combine()"
        for s, t in calls
    ), f"chained call off new Merger(...) not captured: {[(s, label.get(t)) for s, t in calls]}"


# ── Inline-declared receivers (#2346) ─────────────────────────────────────────
# `out T x`, `is T x`, `is not T x`, `case T x:` and switch-arm `T x =>` all
# introduce a binding the receiver table never saw — `x.Method()` on any of
# them silently dropped the edge. `out var x` stays untypable (poison, never a
# guess), and the existing bind/poison conflict rules apply unchanged.


_TWO_GO = (
    "public class Sect { public bool Go() => true; }\n"
    "public class Twig { public bool Go() => false; }\n"
)


def test_out_declared_receiver_resolves(tmp_path):
    """`b.TryGet(out Sect s)` binds s: Sect — `s.Go()` resolves to Sect.Go."""
    calls, r = _calls(tmp_path, {
        "S.cs": (
            _TWO_GO +
            "public class Box { public bool TryGet(out Sect s) { s = new Sect(); return true; } }\n"
            "public class R {\n"
            "    public bool A(Box b) { if (b.TryGet(out Sect s)) { return s.Go(); } return false; }\n"
            "}\n"
        )
    })
    r_a = _find(r, ".A()", "_r_a")
    sect_go = _find(r, ".Go()", "sect")
    twig_go = _find(r, ".Go()", "twig")
    assert (r_a, sect_go) in calls, "out-declared receiver must resolve to its declared type"
    assert (r_a, twig_go) not in calls


def test_out_var_receiver_stays_unbound(tmp_path):
    """`out var v` carries no type name — `v.Go()` must emit NO edge (poison,
    not a guess)."""
    calls, r = _calls(tmp_path, {
        "S.cs": (
            _TWO_GO +
            "public class Box { public bool TryGet(out Sect s) { s = new Sect(); return true; } }\n"
            "public class R {\n"
            "    public bool B(Box b) { b.TryGet(out var v); return v.Go(); }\n"
            "}\n"
        )
    })
    assert not any("_r_b" in s and "go" in t.lower() for s, t in calls), \
        "`out var` receiver is untypable — no edge to either Go()"


def test_is_pattern_receiver_resolves(tmp_path):
    calls, r = _calls(tmp_path, {
        "S.cs": (
            _TWO_GO +
            "public class R {\n"
            "    public bool A(object o) { if (o is Sect s) { return s.Go(); } return false; }\n"
            "}\n"
        )
    })
    r_a = _find(r, ".A()", "_r_a")
    sect_go = _find(r, ".Go()", "sect")
    twig_go = _find(r, ".Go()", "twig")
    assert (r_a, sect_go) in calls, "is-pattern receiver must resolve"
    assert (r_a, twig_go) not in calls


def test_is_not_pattern_receiver_resolves(tmp_path):
    calls, r = _calls(tmp_path, {
        "S.cs": (
            _TWO_GO +
            "public class R {\n"
            "    public bool A(object o) { if (o is not Sect s) { return false; } return s.Go(); }\n"
            "}\n"
        )
    })
    r_a = _find(r, ".A()", "_r_a")
    sect_go = _find(r, ".Go()", "sect")
    twig_go = _find(r, ".Go()", "twig")
    assert (r_a, sect_go) in calls, "is-not-pattern receiver must resolve"
    assert (r_a, twig_go) not in calls


def test_case_pattern_receiver_resolves(tmp_path):
    calls, r = _calls(tmp_path, {
        "S.cs": (
            _TWO_GO +
            "public class R {\n"
            "    public bool A(object o) {\n"
            "        switch (o) { case Sect s: return s.Go(); }\n"
            "        return false;\n"
            "    }\n"
            "}\n"
        )
    })
    r_a = _find(r, ".A()", "_r_a")
    sect_go = _find(r, ".Go()", "sect")
    twig_go = _find(r, ".Go()", "twig")
    assert (r_a, sect_go) in calls, "case-pattern receiver must resolve"
    assert (r_a, twig_go) not in calls


def test_switch_arm_pattern_receiver_resolves(tmp_path):
    calls, r = _calls(tmp_path, {
        "S.cs": (
            _TWO_GO +
            "public class R {\n"
            "    public bool A(object o) {\n"
            "        return o switch { Sect s => s.Go(), _ => false };\n"
            "    }\n"
            "}\n"
        )
    })
    r_a = _find(r, ".A()", "_r_a")
    sect_go = _find(r, ".Go()", "sect")
    twig_go = _find(r, ".Go()", "twig")
    assert (r_a, sect_go) in calls, "switch-expression-arm receiver must resolve"
    assert (r_a, twig_go) not in calls


def test_sibling_pattern_rebind_conflict_poisons(tmp_path):
    """The same name pattern-bound to two DIFFERENT types in one method: raw
    calls carry no lexical position, so neither candidate may win — no edge."""
    calls, r = _calls(tmp_path, {
        "S.cs": (
            _TWO_GO +
            "public class R {\n"
            "    public bool A(object o) {\n"
            "        if (o is Sect x) { return x.Go(); }\n"
            "        if (o is Twig x) { return x.Go(); }\n"
            "        return false;\n"
            "    }\n"
            "}\n"
        )
    })
    r_a = _find(r, ".A()", "_r_a")
    sect_go = _find(r, ".Go()", "sect")
    twig_go = _find(r, ".Go()", "twig")
    assert (r_a, sect_go) not in calls, "conflicting pattern bindings must poison the name"
    assert (r_a, twig_go) not in calls, "conflicting pattern bindings must poison the name"
