mod common;

use brokk_bifrost::IAnalyzer;
use brokk_bifrost::usages::{FuzzyResult, GoUsageGraphStrategy, UsageAnalyzer, UsageFinder};
use common::{definition, go_analyzer_with_files};

#[test]
fn usage_finder_routes_go_targets_through_graph_strategy() {
    let (project, analyzer) = go_analyzer_with_files(&[
        ("util/util.go", "package util\nfunc Helper() {}\n"),
        (
            "main.go",
            r#"
package main

import "example.com/app/util"

func run() {
    util.Helper()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/util.Helper");
    let hits = UsageFinder::new()
        .find_usages_default(&analyzer, std::slice::from_ref(&target))
        .into_either()
        .expect("go graph success");

    assert_eq!(1, hits.len());
    assert!(hits.iter().all(|hit| hit.file == project.file("main.go")));
}

#[test]
fn go_graph_strategy_finds_same_package_references_without_imports() {
    let (project, analyzer) = go_analyzer_with_files(&[
        ("helper.go", "package main\nfunc helper() {}\n"),
        (
            "consumer.go",
            r#"
package main

func run() {
    helper()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app.helper");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("same-package go graph success");

    assert_eq!(1, hits.len());
    assert!(
        hits.iter()
            .all(|hit| hit.file == project.file("consumer.go"))
    );
}

#[test]
fn go_graph_strategy_resolves_qualified_and_aliased_import_selectors() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "config/config.go",
            r#"
package config

const Flag = "on"
var Count = 1
func Build() {}
"#,
        ),
        (
            "main.go",
            r#"
package main

import cfg "example.com/app/config"

func run() {
    cfg.Build()
    _ = cfg.Flag
    cfg.Count = cfg.Count + 1
}
"#,
        ),
    ]);

    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let strategy = GoUsageGraphStrategy::new();
    for fq_name in [
        "example.com/app/config.Build",
        "example.com/app/config._module_.Flag",
        "example.com/app/config._module_.Count",
    ] {
        let target = definition(&analyzer, fq_name);
        let hits = strategy
            .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
            .into_either()
            .unwrap_or_else(|err| panic!("{fq_name} should resolve through alias: {err}"));
        assert!(!hits.is_empty(), "{fq_name} should have graph hits");
    }
}

#[test]
fn go_graph_strategy_resolves_dot_imports_and_ignores_blank_imports() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        ("util/util.go", "package util\nfunc Helper() {}\n"),
        ("sidefx/sidefx.go", "package sidefx\nfunc Helper() {}\n"),
        (
            "main.go",
            r#"
package main

import . "example.com/app/util"
import _ "example.com/app/sidefx"

func run() {
    Helper()
}
"#,
        ),
    ]);

    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let strategy = GoUsageGraphStrategy::new();
    let util_helper = definition(&analyzer, "example.com/app/util.Helper");
    let sidefx_helper = definition(&analyzer, "example.com/app/sidefx.Helper");

    let util_hits = strategy
        .find_usages(
            &analyzer,
            std::slice::from_ref(&util_helper),
            &candidates,
            1000,
        )
        .into_either()
        .expect("dot import should resolve direct helper usage");
    assert_eq!(1, util_hits.len());

    let sidefx_hits = strategy
        .find_usages(
            &analyzer,
            std::slice::from_ref(&sidefx_helper),
            &candidates,
            1000,
        )
        .into_either()
        .expect("blank import query should succeed with no proven hits");
    assert!(
        sidefx_hits.is_empty(),
        "blank imports should not seed direct usages"
    );
}

#[test]
fn go_graph_strategy_resolves_versioned_module_suffix_imports() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "vendor/gopkg.in/yaml.v3/yaml.go",
            "package yaml\nfunc Marshal(in any) []byte { return nil }\n",
        ),
        (
            "main.go",
            r#"
package main

import "gopkg.in/yaml.v3"

func run() {
    _ = yaml.Marshal(nil)
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/vendor/gopkg.in/yaml.v3.Marshal");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("versioned import path should resolve");

    assert_eq!(1, hits.len());
}

#[test]
fn go_graph_strategy_does_not_match_unrelated_same_name_packages() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        ("alpha/service.go", "package alpha\ntype Service struct{}\n"),
        ("beta/service.go", "package beta\ntype Service struct{}\n"),
        (
            "main.go",
            r#"
package main

import "example.com/app/beta"

func run() {
    _ = beta.Service{}
}
"#,
        ),
    ]);

    let alpha = definition(&analyzer, "example.com/app/alpha.Service");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&alpha), &candidates, 1000)
        .into_either()
        .expect("negative query should still succeed");

    assert!(hits.is_empty());
}

#[test]
fn go_graph_strategy_does_not_resolve_external_same_tail_imports_to_local_packages() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        ("model/model.go", "package model\nfunc Helper() {}\n"),
        (
            "main.go",
            r#"
package main

import "github.com/other/model"

func run() {
    model.Helper()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/model.Helper");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("external same-tail import query should succeed");

    assert!(
        hits.is_empty(),
        "external same-tail imports must not resolve to local packages: {hits:?}"
    );
}

#[test]
fn go_graph_strategy_resolves_go_mod_module_imports() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        ("model/model.go", "package model\nfunc Helper() {}\n"),
        (
            "main.go",
            r#"
package main

import "example.com/app/model"

func run() {
    model.Helper()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/model.Helper");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("module import should resolve");

    assert_eq!(1, hits.len(), "module import hits: {hits:?}");
}

#[test]
fn go_graph_strategy_uses_resolved_package_clause_for_unaliased_imports() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "vendor/github.com/go-chi/chi/v5/router.go",
            "package chi\nfunc NewRouter() {}\n",
        ),
        ("internal/foo/foo.go", "package bar\nfunc Helper() {}\n"),
        (
            "main.go",
            r#"
package main

import "github.com/go-chi/chi/v5"
import "example.com/app/internal/foo"

func run() {
    chi.NewRouter()
    bar.Helper()
}
"#,
        ),
    ]);

    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let strategy = GoUsageGraphStrategy::new();

    let router = definition(
        &analyzer,
        "example.com/app/vendor/github.com/go-chi/chi/v5.NewRouter",
    );
    let router_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&router), &candidates, 1000)
        .into_either()
        .expect("semantic import version package clause should resolve");
    assert_eq!(1, router_hits.len(), "router hits: {router_hits:?}");

    let helper = definition(&analyzer, "example.com/app/internal/foo.Helper");
    let helper_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&helper), &candidates, 1000)
        .into_either()
        .expect("directory/package name mismatch should resolve");
    assert_eq!(1, helper_hits.len(), "helper hits: {helper_hits:?}");
}

#[test]
fn go_graph_strategy_respects_explicit_candidate_file_boundaries() {
    let (project, analyzer) = go_analyzer_with_files(&[
        ("util/util.go", "package util\nfunc Helper() {}\n"),
        (
            "a.go",
            r#"
package main

import "example.com/app/util"

func a() {
    util.Helper()
}
"#,
        ),
        (
            "b.go",
            r#"
package main

import "example.com/app/util"

func b() {
    util.Helper()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/util.Helper");
    let candidates = [project.file("a.go")].into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("candidate-limited query should succeed");

    assert_eq!(1, hits.len());
    assert!(hits.iter().all(|hit| hit.file == project.file("a.go")));
}

#[test]
fn go_graph_strategy_builds_from_target_and_candidates_not_unrelated_project_files() {
    let mut files = vec![
        ("util/util.go", "package util\nfunc Helper() {}\n"),
        (
            "main.go",
            r#"
package main

import "example.com/app/util"

func run() {
    util.Helper()
}
"#,
        ),
    ];
    for index in 0..40 {
        let path = Box::leak(format!("unrelated/pkg{index}/noise.go").into_boxed_str());
        let contents =
            Box::leak(format!("package pkg{index}\nfunc Noise{index}() {{}}\n").into_boxed_str());
        files.push((path, contents));
    }
    let (project, analyzer) = go_analyzer_with_files(&files);

    let target = definition(&analyzer, "example.com/app/util.Helper");
    let candidates = [project.file("main.go")].into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("candidate-bounded go graph query should succeed");

    assert_eq!(1, hits.len(), "bounded graph hits: {hits:?}");
    assert!(hits.iter().all(|hit| hit.file == project.file("main.go")));
}

#[test]
fn usage_finder_go_graph_respects_file_filters_as_result_scope() {
    let (project, analyzer) = go_analyzer_with_files(&[
        ("helper.go", "package main\nfunc helper() {}\n"),
        (
            "allowed.go",
            r#"
package main

func allowed() {
    helper()
}
"#,
        ),
        (
            "blocked.go",
            r#"
package main

func blocked() {
    helper()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app.helper");
    let allowed = project.file("allowed.go");
    let hits = UsageFinder::new()
        .with_file_filter(move |file| file == &allowed)
        .find_usages_default(&analyzer, std::slice::from_ref(&target))
        .into_either()
        .expect("filtered go graph query should succeed");

    assert_eq!(1, hits.len(), "filtered hits: {hits:?}");
    assert!(
        hits.iter()
            .all(|hit| hit.file == project.file("allowed.go"))
    );
}

#[test]
fn go_graph_strategy_finds_type_references_in_common_type_positions() {
    let (project, analyzer) = go_analyzer_with_files(&[
        (
            "model/album.go",
            r#"
package model

type Album struct{}
type Box[T any] struct{ Item T }
"#,
        ),
        (
            "core/reader.go",
            r#"
package core

import "example.com/app/model"

type Holder struct {
    Field model.Album
    Items []model.Album
}

type Reader interface {
    Read(model.Album) model.Album
}

func Build(album model.Album) model.Album {
    _ = model.Box[model.Album]{}
    return model.Album{}
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/model.Album");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("type references should resolve");

    assert!(
        hits.len() >= 5,
        "expected multiple type-position hits: {hits:?}"
    );
    assert!(
        hits.iter()
            .all(|hit| hit.file == project.file("core/reader.go"))
    );
}

#[test]
fn go_graph_strategy_finds_type_references_in_pointer_map_channel_and_embedded_fields() {
    let (project, analyzer) = go_analyzer_with_files(&[
        ("model/album.go", "package model\ntype Album struct{}\n"),
        (
            "core/types.go",
            r#"
package core

import "example.com/app/model"

type Holder struct {
    *model.Album
    ByName map[string]model.Album
    Stream chan model.Album
    Receive <-chan *model.Album
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/model.Album");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("expanded type positions should resolve");

    assert!(
        hits.len() >= 4,
        "expected map/channel/pointer/embedded type-position hits: {hits:?}"
    );
    assert!(
        hits.iter()
            .all(|hit| hit.file == project.file("core/types.go"))
    );
}

#[test]
fn go_graph_strategy_finds_methods_and_fields_through_local_receiver_inference() {
    let (project, analyzer) = go_analyzer_with_files(&[
        (
            "model/album.go",
            r#"
package model

type Album struct {
    ImageFiles string
}

func (a Album) Title() string { return "" }
"#,
        ),
        (
            "core/reader.go",
            r#"
package core

import "example.com/app/model"

func Read(album model.Album) string {
    var ptr *model.Album
    album.ImageFiles = "cover.jpg"
    _ = album.ImageFiles
    _ = album.Title()
    _ = ptr.Title()
    return ""
}
"#,
        ),
    ]);

    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let field = definition(&analyzer, "example.com/app/model.Album.ImageFiles");
    let method = definition(&analyzer, "example.com/app/model.Album.Title");
    let strategy = GoUsageGraphStrategy::new();

    let field_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&field), &candidates, 1000)
        .into_either()
        .expect("field references should resolve");
    assert_eq!(2, field_hits.len());
    assert!(
        field_hits
            .iter()
            .all(|hit| hit.file == project.file("core/reader.go"))
    );

    let method_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&method), &candidates, 1000)
        .into_either()
        .expect("method references should resolve");
    assert_eq!(2, method_hits.len());
}

#[test]
fn go_graph_strategy_seeds_members_from_pointer_params_constructors_and_alias_chains() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "model/album.go",
            r#"
package model

type Album struct {
    ImageFiles string
}

func (a *Album) Title() string { return "" }
"#,
        ),
        (
            "core/reader.go",
            r#"
package core

import "example.com/app/model"

func FromPointerParam(album *model.Album) string {
    return album.Title()
}

func FromVar() string {
    var album model.Album
    return album.Title()
}

func FromConstructors() string {
    album := model.Album{}
    ptr := &model.Album{}
    copy := album
    next := copy
    return album.Title() + ptr.Title() + next.Title()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/model.Album.Title");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("receiver seed forms should resolve");

    assert_eq!(5, hits.len(), "receiver seed hits: {hits:?}");
}

#[test]
fn go_graph_strategy_finds_pointer_receiver_method_calls_through_interface_fields() {
    let (project, analyzer) = go_analyzer_with_files(&[
        (
            "example/service.go",
            r#"
package example

type Repository interface {
    Save(value string) string
}

const DefaultPrefix = "job"

var DefaultRepository Repository = MemoryRepository{}

type MemoryRepository struct {
    Last string
}

func (m *MemoryRepository) Save(value string) string {
    m.Last = value
    return value
}

type Service struct {
    repository Repository
}

func NewService(repository Repository) Service {
    return Service{repository: repository}
}

func (s Service) Execute(name string) string {
    stored := s.repository.Save(name)
    return DefaultPrefix + ":" + stored
}
"#,
        ),
        (
            "example/service_test.go",
            r#"
package example

func ExampleService() {
    repository := &MemoryRepository{}
    service := NewService(repository)
    result := service.Execute("Ada")
    _ = DefaultRepository
    _ = DefaultPrefix + result
    repository.Save("Grace")
    _ = repository.Last
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/example.MemoryRepository.Save");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("pointer-receiver method calls through interface fields should resolve");

    assert_eq!(2, hits.len(), "expected both Save calls: {hits:?}");
    assert!(
        hits.iter()
            .any(|hit| hit.file == project.file("example/service.go")),
        "same-file interface-field call should be included: {hits:?}",
    );
    assert!(
        hits.iter()
            .any(|hit| hit.file == project.file("example/service_test.go")),
        "test-file concrete pointer call should be included: {hits:?}",
    );
}

#[test]
fn go_graph_strategy_does_not_match_interface_fields_by_method_name_only() {
    let (_project, analyzer) = go_analyzer_with_files(&[(
        "example/service.go",
        r#"
package example

type StringSaver interface {
    Save(value string) string
}

type IntSaver interface {
    Save(value int) int
}

type MemoryRepository struct{}

func (m *MemoryRepository) Save(value string) string {
    return value
}

type Worker struct {
    saver IntSaver
}

func (w Worker) Run(value int) int {
    return w.saver.Save(value)
}
"#,
    )]);

    let target = definition(&analyzer, "example.com/app/example.MemoryRepository.Save");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("same-name interface methods should be resolved structurally");

    assert!(
        hits.is_empty(),
        "IntSaver.Save(int) must not count as MemoryRepository.Save(string): {hits:?}",
    );
}

// Regression for #232: a value-receiver method called on a local that is bound to
// a constructor's return value (`service := NewService()`) must resolve on the
// graph path. Before the constructor-return seeding it returned zero hits and the
// regex fallback masked the gap.
#[test]
fn go_graph_strategy_finds_value_receiver_calls_on_constructor_locals() {
    let (project, analyzer) = go_analyzer_with_files(&[
        (
            "example/service.go",
            r#"
package example

type Service struct {
    Name string
}

func (s Service) Execute() string {
    return s.Name
}

func NewService() Service {
    return Service{Name: "demo"}
}
"#,
        ),
        (
            "example/service_test.go",
            r#"
package example

import "testing"

func TestExecute(t *testing.T) {
    service := NewService()
    if service.Execute() != "demo" {
        t.Fatal("unexpected result")
    }
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/example.Service.Execute");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect(
            "value-receiver call on a constructor-returned local should resolve on the graph path",
        );

    assert_eq!(
        1,
        hits.len(),
        "expected the service.Execute() call: {hits:?}"
    );
    assert!(
        hits.iter()
            .all(|hit| hit.file == project.file("example/service_test.go")),
        "hit should be the test-file call site: {hits:?}",
    );
}

// A value-receiver constructor returning the common `(Owner, error)` tuple should
// also seed the receiver from its first result.
#[test]
fn go_graph_strategy_finds_value_receiver_calls_on_tuple_constructor_locals() {
    let (project, analyzer) = go_analyzer_with_files(&[
        (
            "example/service.go",
            r#"
package example

type Service struct {
    Name string
}

func (s Service) Execute() string {
    return s.Name
}

func NewService() (Service, error) {
    return Service{Name: "demo"}, nil
}
"#,
        ),
        (
            "consumer/consumer.go",
            r#"
package consumer

import "example.com/app/example"

func Run() string {
    service, _ := example.NewService()
    return service.Execute()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/example.Service.Execute");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect(
            "value-receiver call on a tuple-constructor local should resolve on the graph path",
        );

    assert_eq!(
        1,
        hits.len(),
        "expected the service.Execute() call: {hits:?}"
    );
    assert!(
        hits.iter()
            .all(|hit| hit.file == project.file("consumer/consumer.go")),
        "hit should be the consumer call site: {hits:?}",
    );
}

// A local/parameter that shadows the package constructor name is not the package
// constructor, so a method call on a local bound to it must not be a hit.
#[test]
fn go_graph_strategy_does_not_seed_local_shadowing_a_constructor_name() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "example/service.go",
            r#"
package example

type Service struct {
    Name string
}

func (s Service) Execute() string {
    return s.Name
}

func NewService() Service {
    return Service{Name: "demo"}
}
"#,
        ),
        (
            "example/consumer.go",
            r#"
package example

func Run(NewService func() string) {
    x := NewService()
    _ = x.Execute()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/example.Service.Execute");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("graph should resolve");

    assert!(
        hits.is_empty(),
        "x.Execute() where NewService is a shadowing parameter must not be a hit: {hits:?}",
    );
}

#[test]
fn go_graph_strategy_keeps_blank_lhs_constructor_seeds_positional() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "example/service.go",
            r#"
package example

type Service struct{}

func (s Service) Execute() string { return "service" }

func NewService() Service { return Service{} }
"#,
        ),
        (
            "example/other.go",
            r#"
package example

type Other struct{}

func (o Other) Execute() string { return "other" }

func NewOther() Other { return Other{} }
"#,
        ),
        (
            "example/consumer.go",
            r#"
package example

func Run() string {
    _, other := NewService(), NewOther()
    return other.Execute()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/example.Service.Execute");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("graph should resolve");

    assert!(
        hits.is_empty(),
        "blank LHS must not shift NewService's receiver proof onto other: {hits:?}",
    );
}

#[test]
fn go_graph_strategy_resolves_constructor_before_short_var_lhs_scope() {
    let (project, analyzer) = go_analyzer_with_files(&[
        (
            "example/service.go",
            r#"
package example

type Service struct{}

func (s Service) Execute() string { return "service" }

func NewService() Service { return Service{} }
"#,
        ),
        (
            "example/consumer.go",
            r#"
package example

func Run() string {
    NewService, service := 0, NewService()
    _ = NewService
    return service.Execute()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/example.Service.Execute");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("graph should resolve");

    assert_eq!(
        1,
        hits.len(),
        "short-var LHS names should not shadow RHS constructor calls: {hits:?}",
    );
    assert!(
        hits.iter()
            .all(|hit| hit.file == project.file("example/consumer.go")),
        "hit should be the consumer call site: {hits:?}",
    );
}

#[test]
fn go_graph_strategy_respects_grouped_var_spec_constructor_shadowing() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "example/service.go",
            r#"
package example

type Service struct{}

func (s Service) Execute() string { return "service" }

func NewService() Service { return Service{} }
"#,
        ),
        (
            "example/other.go",
            r#"
package example

type Other struct{}

func (o Other) Execute() string { return "other" }
"#,
        ),
        (
            "example/consumer.go",
            r#"
package example

func Run() string {
    var (
        NewService = func() Other { return Other{} }
        service = NewService()
    )
    return service.Execute()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/example.Service.Execute");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("graph should resolve");

    assert!(
        hits.is_empty(),
        "later grouped var specs must see earlier constructor-shadowing specs: {hits:?}",
    );
}

#[test]
fn go_graph_strategy_keeps_mixed_multi_assignment_receiver_proofs_positional() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "model/album.go",
            r#"
package model

type Album struct{}

func (a Album) Title() string { return "" }
"#,
        ),
        (
            "other/album.go",
            r#"
package other

type Album struct{}

func (a Album) Title() string { return "" }
"#,
        ),
        (
            "core/reader.go",
            r#"
package core

import "example.com/app/model"
import "example.com/app/other"

func Read() string {
    album, otherAlbum := model.Album{}, other.Album{}
    return album.Title() + otherAlbum.Title()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/model.Album.Title");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("mixed multi-assignment receiver query should succeed");

    assert_eq!(
        1,
        hits.len(),
        "only the positionally matched model receiver should count: {hits:?}"
    );
}

#[test]
fn go_graph_strategy_seeds_members_from_grouped_and_multi_name_var_declarations() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "model/album.go",
            r#"
package model

type Album struct{}

func (a Album) Title() string { return "" }
"#,
        ),
        (
            "core/reader.go",
            r#"
package core

import "example.com/app/model"

func Read() string {
    var (
        album model.Album
        first,
            second /* receiver type comment */ model.Album
    )
    return album.Title() + first.Title() + second.Title()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/model.Album.Title");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("grouped and multi-name var receivers should resolve");

    assert_eq!(3, hits.len(), "var receiver hits: {hits:?}");
}

#[test]
fn go_graph_strategy_keeps_member_receiver_proofs_conservative() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "model/album.go",
            r#"
package model

type Album struct {
    ImageFiles string
}

func (a Album) Title() string { return "" }
"#,
        ),
        (
            "other/album.go",
            r#"
package other

type Album struct {
    ImageFiles string
}

func (a Album) Title() string { return "" }
"#,
        ),
        (
            "core/reader.go",
            r#"
package core

import "example.com/app/model"
import "example.com/app/other"

type Wrapper struct {
    model.Album
}

func readUnknown(album any) string {
    return album.Title()
}

func readOther(otherAlbum other.Album) string {
    return otherAlbum.ImageFiles + otherAlbum.Title()
}

func readInterface() string {
    var x interface{ Title() string }
    return x.Title()
}

func readEmbedded(wrapper Wrapper) string {
    return wrapper.ImageFiles + wrapper.Title()
}
"#,
        ),
    ]);

    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let field = definition(&analyzer, "example.com/app/model.Album.ImageFiles");
    let method = definition(&analyzer, "example.com/app/model.Album.Title");
    let strategy = GoUsageGraphStrategy::new();

    let field_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&field), &candidates, 1000)
        .into_either()
        .expect("field negative query should succeed");
    assert!(
        field_hits.is_empty(),
        "unproven, unrelated, and embedded-promoted fields should not count"
    );

    let method_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&method), &candidates, 1000)
        .into_either()
        .expect("method negative query should succeed");
    assert!(
        method_hits.is_empty(),
        "dynamic interface, unrelated owner, and embedded-promoted methods should not count"
    );
}

#[test]
fn go_graph_strategy_respects_local_shadowing_of_imported_package_aliases_and_dot_imports() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        ("model/model.go", "package model\nfunc Helper() {}\n"),
        (
            "core/reader.go",
            r#"
package core

import model "example.com/app/model"
import . "example.com/app/model"

type local struct{}
func (local) Helper() {}

func shadowPackageAlias() {
    model := local{}
    model.Helper()
}

func shadowDotImport() {
    Helper := func() {}
    Helper()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/model.Helper");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("shadowing query should succeed");

    assert!(
        hits.is_empty(),
        "local shadows should block imported package and dot-import proofs"
    );
}

#[test]
fn go_graph_strategy_finds_function_usage_forms_across_call_contexts() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        ("util/util.go", "package util\nfunc Helper() {}\n"),
        (
            "local.go",
            r#"
package main

func helper() {}

func samePackage() {
    helper()
    f := helper
    f()
}
"#,
        ),
        (
            "main.go",
            r#"
package main

import "example.com/app/util"
import . "example.com/app/util"

func callForms() {
    util.Helper()
    Helper()
    deferred := func() {
        util.Helper()
    }
    deferred()
    defer util.Helper()
    go util.Helper()
}
"#,
        ),
    ]);

    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let strategy = GoUsageGraphStrategy::new();

    let imported = definition(&analyzer, "example.com/app/util.Helper");
    let imported_hits = strategy
        .find_usages(
            &analyzer,
            std::slice::from_ref(&imported),
            &candidates,
            1000,
        )
        .into_either()
        .expect("imported function forms should resolve");
    assert_eq!(5, imported_hits.len(), "imported hits: {imported_hits:?}");

    let local = definition(&analyzer, "example.com/app.helper");
    let local_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&local), &candidates, 1000)
        .into_either()
        .expect("same-package function values should resolve");
    assert_eq!(2, local_hits.len(), "local function hits: {local_hits:?}");
}

#[test]
fn go_graph_strategy_keeps_function_usage_shadowing_conservative() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        ("util/util.go", "package util\nfunc Helper() {}\n"),
        (
            "main.go",
            r#"
package main

import util "example.com/app/util"
import . "example.com/app/util"

func shadowPackageAlias() {
    util := struct{ Helper func() }{Helper: func() {}}
    util.Helper()
}

func shadowDotImport() {
    Helper := func() {}
    Helper()
}

func shadowParameter(Helper func()) {
    Helper()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/util.Helper");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("shadowed function query should succeed");

    assert!(
        hits.is_empty(),
        "local shadows should block imported function proofs: {hits:?}"
    );
}

#[test]
fn go_graph_strategy_keeps_same_package_top_level_shadowing_conservative() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "defs.go",
            r#"
package main

const Flag = "global"

func helper() {}
"#,
        ),
        (
            "consumer.go",
            r#"
package main

func localShadows() {
    helper := func() {}
    helper()
    Flag := "local"
    _ = Flag
}
"#,
        ),
    ]);

    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let strategy = GoUsageGraphStrategy::new();

    let helper = definition(&analyzer, "example.com/app.helper");
    let helper_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&helper), &candidates, 1000)
        .into_either()
        .expect("same-package function shadowing query should succeed");
    assert!(
        helper_hits.is_empty(),
        "same-package function shadows should not count: {helper_hits:?}"
    );

    let flag = definition(&analyzer, "example.com/app._module_.Flag");
    let flag_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&flag), &candidates, 1000)
        .into_either()
        .expect("same-package const shadowing query should succeed");
    assert!(
        flag_hits.is_empty(),
        "same-package const shadows should not count: {flag_hits:?}"
    );
}

#[test]
fn go_graph_strategy_finds_top_level_var_and_const_usage_forms() {
    let (project, analyzer) = go_analyzer_with_files(&[
        (
            "config/config.go",
            r#"
package config

const Flag = "on"
var Count = 1
"#,
        ),
        (
            "config/internal.go",
            r#"
package config

func samePackage() {
    _ = Flag
    Count += 1
}
"#,
        ),
        (
            "other/config.go",
            r#"
package other

const Flag = "other"
var Count = 99
"#,
        ),
        (
            "main.go",
            r#"
package main

import cfg "example.com/app/config"
import other "example.com/app/other"

func external() {
    _ = cfg.Flag
    cfg.Count = cfg.Count + 1
    _ = &cfg.Count
    _ = other.Flag
    other.Count += 1
}
"#,
        ),
    ]);

    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let strategy = GoUsageGraphStrategy::new();

    let flag = definition(&analyzer, "example.com/app/config._module_.Flag");
    let flag_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&flag), &candidates, 1000)
        .into_either()
        .expect("const usages should resolve");
    assert_eq!(2, flag_hits.len(), "flag hits: {flag_hits:?}");
    assert!(
        flag_hits
            .iter()
            .all(|hit| hit.file == project.file("config/internal.go")
                || hit.file == project.file("main.go"))
    );

    let count = definition(&analyzer, "example.com/app/config._module_.Count");
    let count_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&count), &candidates, 1000)
        .into_either()
        .expect("var usages should resolve");
    assert_eq!(4, count_hits.len(), "count hits: {count_hits:?}");
    assert!(
        count_hits
            .iter()
            .all(|hit| hit.file == project.file("config/internal.go")
                || hit.file == project.file("main.go"))
    );
}

#[test]
fn go_graph_strategy_finds_type_references_in_advanced_type_positions() {
    let (project, analyzer) = go_analyzer_with_files(&[
        (
            "model/album.go",
            r#"
package model

type Album struct{}
type Box[T any] struct{ Item T }
"#,
        ),
        (
            "core/types.go",
            r#"
package core

import "example.com/app/model"

type Alias = model.Album
type Constraint interface {
    ~[]model.Album
    Accept(model.Album) *model.Album
}
type Handler func(model.Album) model.Album
type Uses struct {
    Fixed [2]model.Album
    Boxed model.Box[model.Album]
}

func Check(v any) {
    _ = v.(model.Album)
    switch v.(type) {
    case model.Album:
    }
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/model.Album");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("advanced type positions should resolve");

    assert!(
        hits.len() >= 9,
        "expected alias, constraint, function, array, generic, assertion, and switch hits: {hits:?}"
    );
    assert!(
        hits.iter()
            .all(|hit| hit.file == project.file("core/types.go"))
    );
}

#[test]
fn go_graph_strategy_finds_member_usages_in_nested_receiver_contexts() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "model/album.go",
            r#"
package model

type Album struct {
    ImageFiles string
}

func (a Album) Title() string { return "" }
"#,
        ),
        (
            "core/reader.go",
            r#"
package core

import "example.com/app/model"

func NamedReturn() (album model.Album) {
    _ = album.Title()
    album.ImageFiles = "cover.jpg"
    return
}

func Nested(album model.Album) {
    func() {
        alias := album
        _ = alias.Title()
        _ = alias.ImageFiles
    }()
    {
        next := album
        _ = next.Title()
        next.ImageFiles = "back.jpg"
    }
}
"#,
        ),
    ]);

    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let strategy = GoUsageGraphStrategy::new();

    let method = definition(&analyzer, "example.com/app/model.Album.Title");
    let method_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&method), &candidates, 1000)
        .into_either()
        .expect("nested method receiver usages should resolve");
    assert_eq!(3, method_hits.len(), "method hits: {method_hits:?}");

    let field = definition(&analyzer, "example.com/app/model.Album.ImageFiles");
    let field_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&field), &candidates, 1000)
        .into_either()
        .expect("nested field receiver usages should resolve");
    assert_eq!(3, field_hits.len(), "field hits: {field_hits:?}");
}

#[test]
fn go_graph_strategy_finds_member_usages_through_pointer_dereference_receivers() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "model/album.go",
            r#"
package model

type Album struct {
    ImageFiles string
}

func (a *Album) Title() string { return "" }
"#,
        ),
        (
            "core/reader.go",
            r#"
package core

import "example.com/app/model"

func Read(album *model.Album) string {
    _ = (*album).Title()
    (*album).ImageFiles = "cover.jpg"
    return album.Title()
}
"#,
        ),
    ]);

    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let strategy = GoUsageGraphStrategy::new();

    let method = definition(&analyzer, "example.com/app/model.Album.Title");
    let method_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&method), &candidates, 1000)
        .into_either()
        .expect("dereferenced method receiver usages should resolve");
    assert_eq!(2, method_hits.len(), "method hits: {method_hits:?}");

    let field = definition(&analyzer, "example.com/app/model.Album.ImageFiles");
    let field_hits = strategy
        .find_usages(&analyzer, std::slice::from_ref(&field), &candidates, 1000)
        .into_either()
        .expect("dereferenced field receiver usages should resolve");
    assert_eq!(1, field_hits.len(), "field hits: {field_hits:?}");
}

#[test]
fn go_graph_strategy_keeps_interprocedural_member_assignment_out_of_scope() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        (
            "model/album.go",
            r#"
package model

type Album struct {
    ImageFiles string
}

func (a Album) Title() string { return "" }
"#,
        ),
        (
            "core/reader.go",
            r#"
package core

import "example.com/app/model"

var saved any

func Save(album model.Album) {
    saved = album
}

func Later() string {
    return saved.Title()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app/model.Album.Title");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let hits = GoUsageGraphStrategy::new()
        .find_usages(&analyzer, std::slice::from_ref(&target), &candidates, 1000)
        .into_either()
        .expect("interprocedural negative query should succeed");

    assert!(
        hits.is_empty(),
        "interprocedural data-flow should remain out of scope: {hits:?}"
    );
}

#[test]
fn go_graph_strategy_enforces_max_usages_limit() {
    let (_project, analyzer) = go_analyzer_with_files(&[
        ("helper.go", "package main\nfunc helper() {}\n"),
        (
            "consumer.go",
            r#"
package main

func run() {
    helper()
    helper()
}
"#,
        ),
    ]);

    let target = definition(&analyzer, "example.com/app.helper");
    let candidates = analyzer.get_analyzed_files().into_iter().collect();
    let result = GoUsageGraphStrategy::new().find_usages(
        &analyzer,
        std::slice::from_ref(&target),
        &candidates,
        1,
    );

    assert!(matches!(
        result,
        FuzzyResult::TooManyCallsites {
            total_callsites: 2,
            limit: 1,
            ..
        }
    ));
}
