"""Tests for context_builder.py."""

import json
from pathlib import Path

import pytest

from agents_md_mcp.cache import make_empty_cache
from agents_md_mcp.config import SIZE_PROFILES, load_config
from agents_md_mcp.aggregator import _aggregate_by_directory, _extract_class_pattern, _is_dto_directory
from agents_md_mcp.build_system import _detect_build_systems
from agents_md_mcp.context_builder import build_payload, _build_interface_impl_map
from agents_md_mcp.project_scanner import _detect_wiring, _cap_routes, _scan_project_structure
from agents_md_mcp.symbol_utils import _passes_threshold
from agents_md_mcp.models import CachedFile, FileAnalysis, FileChange, SymbolInfo

_MEDIUM = SIZE_PROFILES["medium"]


# ── Helpers ───────────────────────────────────────────────────────────────────

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


def _analysis(path: str, lang: str = "python", symbols: list[SymbolInfo] | None = None) -> FileAnalysis:
    return FileAnalysis(path=path, language=lang, symbols=symbols or [])


def _sym(name: str, kind: str = "function", visibility: str = "public", sig: str = "sig", parent: str | None = None) -> SymbolInfo:
    return SymbolInfo(name=name, kind=kind, visibility=visibility, signature=sig, parent=parent)  # type: ignore[arg-type]


# ── _passes_threshold ─────────────────────────────────────────────────────────

def test_threshold_high_passes_all() -> None:
    assert _passes_threshold("high", "low")
    assert _passes_threshold("high", "medium")
    assert _passes_threshold("high", "high")


def test_threshold_low_only_passes_low() -> None:
    assert _passes_threshold("low", "low")
    assert not _passes_threshold("low", "medium")
    assert not _passes_threshold("low", "high")


def test_threshold_medium() -> None:
    assert _passes_threshold("medium", "low")
    assert _passes_threshold("medium", "medium")
    assert not _passes_threshold("medium", "high")


# ── _detect_build_systems ────────────────────────────────────────────────────

def test_detects_npm(tmp_path: Path) -> None:
    _write(tmp_path / "package.json", json.dumps({
        "scripts": {"build": "vite build", "test": "vitest"}
    }))
    result = _detect_build_systems(tmp_path)
    assert "npm" in result["detected"]
    assert "build" in result["scripts"].get("npm", {})


def test_detects_python(tmp_path: Path) -> None:
    _write(tmp_path / "pyproject.toml", "[build-system]\n")
    result = _detect_build_systems(tmp_path)
    assert "python" in result["detected"]


def test_detects_go(tmp_path: Path) -> None:
    _write(tmp_path / "go.mod", "module example.com/app\n")
    result = _detect_build_systems(tmp_path)
    assert "go" in result["detected"]


def test_detects_makefile_targets(tmp_path: Path) -> None:
    _write(tmp_path / "Makefile", "build:\n\tgo build\ntest:\n\tgo test ./...\n")
    result = _detect_build_systems(tmp_path)
    assert "make" in result["detected"]
    assert "build" in result["scripts"].get("make", {})
    assert "test" in result["scripts"].get("make", {})


def test_no_build_system(tmp_path: Path) -> None:
    result = _detect_build_systems(tmp_path)
    assert result["detected"] == []


def _csproj(target: str = "net8.0", output_type: str | None = None, packages: list[tuple[str, str]] = [], proj_refs: list[str] = []) -> str:
    pkg_lines = "\n".join(
        f'    <PackageReference Include="{n}" Version="{v}" />' for n, v in packages
    )
    ref_lines = "\n".join(
        f'    <ProjectReference Include="{r}" />' for r in proj_refs
    )
    output_el = f"  <OutputType>{output_type}</OutputType>" if output_type else ""
    return f"""<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>{target}</TargetFramework>
    {output_el}
  </PropertyGroup>
  <ItemGroup>
{pkg_lines}
{ref_lines}
  </ItemGroup>
</Project>"""


def test_dotnet_parses_target_framework(tmp_path: Path) -> None:
    _write(tmp_path / "MyApp" / "MyApp.csproj", _csproj(target="net8.0"))
    result = _detect_build_systems(tmp_path)
    assert "dotnet" in result["detected"]
    projects = result.get("dotnet_projects", [])
    assert len(projects) == 1
    assert projects[0]["target_framework"] == "net8.0"


def test_dotnet_parses_output_type(tmp_path: Path) -> None:
    _write(tmp_path / "MyApi" / "MyApi.csproj", _csproj(output_type="Exe"))
    result = _detect_build_systems(tmp_path)
    assert result["dotnet_projects"][0]["output_type"] == "Exe"


def test_dotnet_parses_packages(tmp_path: Path) -> None:
    pkgs = [("MediatR", "12.0.0"), ("AutoMapper", "13.0.1")]
    _write(tmp_path / "App" / "App.csproj", _csproj(packages=pkgs))
    result = _detect_build_systems(tmp_path)
    packages = result["dotnet_projects"][0]["packages"]
    assert "MediatR" in packages
    assert "AutoMapper" in packages


def test_dotnet_caps_packages_at_15(tmp_path: Path) -> None:
    pkgs = [(f"Package{i}", f"1.0.{i}") for i in range(20)]
    _write(tmp_path / "App" / "App.csproj", _csproj(packages=pkgs))
    result = _detect_build_systems(tmp_path)
    assert len(result["dotnet_projects"][0]["packages"]) == 15


def test_dotnet_parses_project_references(tmp_path: Path) -> None:
    refs = ["../MyDomain/MyDomain.csproj", "../MyInfra/MyInfra.csproj"]
    _write(tmp_path / "MyApi" / "MyApi.csproj", _csproj(proj_refs=refs))
    result = _detect_build_systems(tmp_path)
    proj_refs = result["dotnet_projects"][0]["project_references"]
    assert "../MyDomain/MyDomain.csproj" in proj_refs
    assert "../MyInfra/MyInfra.csproj" in proj_refs


def test_dotnet_no_projects_key_when_no_csproj(tmp_path: Path) -> None:
    _write(tmp_path / "MyApp.sln", "")
    result = _detect_build_systems(tmp_path)
    assert "dotnet" in result["detected"]
    assert "dotnet_projects" not in result


def test_dotnet_framework_style_csproj(tmp_path: Path) -> None:
    content = """<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <OutputType>Library</OutputType>
    <TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="MyCustomLib">
      <HintPath>../packages/MyCustomLib.dll</HintPath>
    </Reference>
    <Reference Include="AnotherLib">
      <HintPath>../packages/AnotherLib.dll</HintPath>
    </Reference>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="../OtherProject/OtherProject.csproj">
      <Project>{abc}</Project>
      <Name>OtherProject</Name>
    </ProjectReference>
  </ItemGroup>
</Project>"""
    _write(tmp_path / "App" / "App.csproj", content)
    result = _detect_build_systems(tmp_path)
    proj = result["dotnet_projects"][0]
    assert proj["target_framework"] == "v4.8"
    assert proj["output_type"] == "Library"
    assert "MyCustomLib" in proj["packages"]
    assert "AnotherLib" in proj["packages"]
    assert "System" not in proj["packages"]
    assert "../OtherProject/OtherProject.csproj" in proj["project_references"]


# ── Go packages ──────────────────────────────────────────────────────────────

def test_go_extracts_direct_deps(tmp_path: Path) -> None:
    _write(tmp_path / "go.mod", """\
module github.com/myorg/myapp

go 1.22

require (
\tgithub.com/gin-gonic/gin v1.9.1
\tgithub.com/jmoiron/sqlx v1.3.5
\tgolang.org/x/sync v0.6.0 // indirect
)
""")
    result = _detect_build_systems(tmp_path)
    assert "go" in result["detected"]
    pkgs = result["go_packages"]
    assert "github.com/gin-gonic/gin" in pkgs
    assert "github.com/jmoiron/sqlx" in pkgs
    # indirect deps should be excluded
    assert "golang.org/x/sync" not in pkgs


def test_go_single_line_require(tmp_path: Path) -> None:
    _write(tmp_path / "go.mod", """\
module example.com/app

go 1.21

require github.com/stretchr/testify v1.8.4
""")
    result = _detect_build_systems(tmp_path)
    assert "github.com/stretchr/testify" in result["go_packages"]


def test_go_no_deps_no_key(tmp_path: Path) -> None:
    _write(tmp_path / "go.mod", """\
module example.com/app

go 1.21
""")
    result = _detect_build_systems(tmp_path)
    assert "go" in result["detected"]
    assert "go_packages" not in result


# ── _scan_project_structure ───────────────────────────────────────────────────

def test_scans_root_files(tmp_path: Path) -> None:
    _write(tmp_path / "README.md")
    _write(tmp_path / "main.py")
    cfg = load_config(tmp_path)
    result = _scan_project_structure(tmp_path, cfg)
    assert "README.md" in result["root_files"] or "main.py" in result["root_files"]


def test_detects_ci_files(tmp_path: Path) -> None:
    ci_file = tmp_path / ".github" / "workflows" / "ci.yml"
    _write(ci_file, "name: CI\n")
    cfg = load_config(tmp_path)
    result = _scan_project_structure(tmp_path, cfg)
    assert any("ci.yml" in f for f in result["ci_files_found"])


def test_detects_test_directories(tmp_path: Path) -> None:
    (tmp_path / "tests").mkdir()
    cfg = load_config(tmp_path)
    result = _scan_project_structure(tmp_path, cfg)
    assert any("tests" in d for d in result["test_directories"])


def test_directory_language_detection(tmp_path: Path) -> None:
    _write(tmp_path / "src" / "app.py", "# python")
    _write(tmp_path / "src" / "utils.py", "# python")
    cfg = load_config(tmp_path)
    result = _scan_project_structure(tmp_path, cfg)
    src_entry = result["directories"].get("src/")
    assert src_entry is not None
    assert src_entry["languages"] == "python"


# ── build_payload ─────────────────────────────────────────────────────────────

def test_build_payload_new_files(tmp_path: Path) -> None:
    _write(tmp_path / "src" / "app.py", "class App:\n  def run(self): pass")
    cfg = load_config(tmp_path)

    changes = [FileChange(path="src/app.py", status="new", new_hash="abc")]
    analyses = {"src/app.py": _analysis("src/app.py", "python", [
        _sym("App", kind="class"),
        _sym("run", kind="method", sig="public void run()", parent="App"),
    ])}

    payload = build_payload(tmp_path, cfg, changes, analyses, cache=None)

    assert len(payload["full_analysis"]) == 1
    assert payload["full_analysis"][0]["file"] == "src/app.py"


def test_build_payload_does_not_include_agents_md_fields(tmp_path: Path) -> None:
    cfg = load_config(tmp_path)
    payload = build_payload(tmp_path, cfg, [], {}, cache=None)
    assert "instructions" not in payload
    assert "existing_agents_md" not in payload


def test_build_payload_modified_with_diff(tmp_path: Path) -> None:
    _write(tmp_path / "src" / "app.py", "def run(): pass\ndef stop(): pass")
    cfg = load_config(tmp_path)

    old_analysis = _analysis("src/app.py", "python", [_sym("run", sig="def run()")])
    cache = make_empty_cache()
    from agents_md_mcp.models import CachedSymbol
    cache.files["src/app.py"] = CachedFile(
        hash="old",
        symbols=[CachedSymbol(name="run", kind="function", visibility="public", signature="def run()")],
    )

    new_analysis = _analysis("src/app.py", "python", [
        _sym("run", sig="def run()"),
        _sym("stop", sig="def stop()"),
    ])

    changes = [FileChange(path="src/app.py", status="modified", old_hash="old", new_hash="new")]
    analyses = {"src/app.py": new_analysis}

    payload = build_payload(tmp_path, cfg, changes, analyses, cache=cache, scan_type="incremental")

    assert len(payload["changes"]) == 1
    diff_entry = payload["changes"][0]
    assert diff_entry["status"] == "modified"
    assert any(s["name"] == "stop" for s in diff_entry["diff"]["added_symbols"])


def test_build_payload_deleted_file(tmp_path: Path) -> None:
    cfg = load_config(tmp_path)
    changes = [FileChange(path="src/old.py", status="deleted", old_hash="abc")]
    payload = build_payload(tmp_path, cfg, changes, {}, cache=None)
    assert len(payload["changes"]) == 1
    assert payload["changes"][0]["status"] == "deleted"
    assert payload["changes"][0]["impact"] == "high"


# ── _aggregate_by_directory ────────────────────────────────────────────────────

def _file_entry(file: str, lang: str, symbols: list[dict] | None = None) -> dict:
    return {"file": file, "language": lang, "symbols": symbols or []}


def _sym_dict(name: str, kind: str = "function") -> dict:
    return {"name": name, "kind": kind, "visibility": "public"}


def test_aggregate_below_threshold_keeps_individual() -> None:
    entries = [
        _file_entry("src/a.py", "python"),
        _file_entry("src/b.py", "python"),
    ]
    result = _aggregate_by_directory(entries, threshold=8, profile=_MEDIUM)
    # Below threshold — must remain as individual file entries
    assert len(result) == 2
    assert all(e.get("kind") != "directory_summary" for e in result)


def test_aggregate_above_threshold_produces_summary() -> None:
    shared_methods = ["public void get()", "public void save()", "public void delete()"]
    entries = [
        _file_entry(f"src/repo{i}.py", "python", [
            {"name": f"Repo{i}", "kind": "class", "visibility": "public", "methods": shared_methods}
        ])
        for i in range(8)
    ]
    result = _aggregate_by_directory(entries, threshold=8, profile=_MEDIUM)
    assert len(result) == 1
    summary = result[0]
    assert summary["kind"] == "directory_summary"
    assert summary["file_count"] == 8
    assert summary["language"] == "python"
    assert "public void get()" in summary["common_methods"]


def test_aggregate_weak_pattern_produces_fallback_summary() -> None:
    # Each file has completely different symbols — no common methods.
    # With the generic fallback, this still produces a directory_summary.
    entries = [
        _file_entry(f"src/f{i}.py", "python", [_sym_dict(f"unique_{i}")])
        for i in range(8)
    ]
    result = _aggregate_by_directory(entries, threshold=8, profile=_MEDIUM)
    summaries = [e for e in result if e.get("kind") == "directory_summary"]
    assert len(summaries) == 1
    assert summaries[0]["file_count"] == 8
    assert summaries[0]["note"] == "No common method pattern detected"


def test_aggregate_minority_language_kept_individual() -> None:
    shared_syms = [_sym_dict("get"), _sym_dict("save"), _sym_dict("delete")]
    py_entries = [_file_entry(f"src/repo{i}.py", "python", shared_syms) for i in range(8)]
    ts_entry = _file_entry("src/utils.ts", "typescript", [_sym_dict("helper")])
    result = _aggregate_by_directory(py_entries + [ts_entry], threshold=8, profile=_MEDIUM)

    kinds = [e.get("kind") for e in result]
    assert "directory_summary" in kinds
    # The TypeScript file must remain as an individual entry
    ts_result = [e for e in result if e.get("file") == "src/utils.ts"]
    assert len(ts_result) == 1


def test_aggregate_different_dirs_are_independent() -> None:
    shared_syms = [_sym_dict("get"), _sym_dict("save"), _sym_dict("delete")]
    entries = (
        [_file_entry(f"services/s{i}.py", "python", shared_syms) for i in range(8)]
        + [_file_entry(f"models/m{i}.py", "python", shared_syms) for i in range(8)]
    )
    result = _aggregate_by_directory(entries, threshold=8, profile=_MEDIUM)
    summaries = [e for e in result if e.get("kind") == "directory_summary"]
    assert len(summaries) == 2
    dirs = {s["directory"] for s in summaries}
    assert any("services" in d for d in dirs)
    assert any("models" in d for d in dirs)


# ── _extract_class_pattern ────────────────────────────────────────────────────

def test_extract_class_pattern_common_suffix() -> None:
    entries = [
        _file_entry("a.py", "python", [_sym_dict("OrderService", "class")]),
        _file_entry("b.py", "python", [_sym_dict("UserService", "class")]),
        _file_entry("c.py", "python", [_sym_dict("PaymentService", "class")]),
    ]
    pattern = _extract_class_pattern(entries)
    assert pattern is not None
    assert pattern["pattern"] == "*Service"
    assert pattern["total"] == 3
    assert len(pattern["examples"]) == 3
    assert "OrderService" in pattern["examples"]


def test_extract_class_pattern_common_prefix() -> None:
    entries = [
        _file_entry("a.py", "python", [_sym_dict("AbstractOrder", "class")]),
        _file_entry("b.py", "python", [_sym_dict("AbstractUser", "class")]),
        _file_entry("c.py", "python", [_sym_dict("AbstractPayment", "class")]),
    ]
    pattern = _extract_class_pattern(entries)
    assert pattern is not None
    assert pattern["pattern"] == "Abstract*"
    assert pattern["total"] == 3


def test_extract_class_pattern_partial_match() -> None:
    # 3 match out of 4 (75% - below 80% max threshold min 2)
    # Wait, max(2, int(4 * 0.8)) = max(2, 3) = 3
    # So 3 out of 4 is 75%, but threshold for 4 is 3. So it SHOULD match!
    entries = [
        _file_entry("a.py", "python", [_sym_dict("CreateRequest", "class")]),
        _file_entry("b.py", "python", [_sym_dict("UpdateRequest", "class")]),
        _file_entry("c.py", "python", [_sym_dict("DeleteRequest", "class")]),
        _file_entry("d.py", "python", [_sym_dict("RequestWrapper", "class")]),
    ]
    pattern = _extract_class_pattern(entries)
    assert pattern is not None
    assert pattern["pattern"] == "*Request"
    assert pattern["total"] == 3
    assert "CreateRequest" in pattern["examples"]
    assert "RequestWrapper" not in pattern["examples"]


def test_extract_class_pattern_no_pattern() -> None:
    entries = [
        _file_entry("a.py", "python", [_sym_dict("Foo", "class")]),
        _file_entry("b.py", "python", [_sym_dict("Bar", "class")]),
        _file_entry("c.py", "python", [_sym_dict("Baz", "class")]),
    ]
    pattern = _extract_class_pattern(entries)
    assert pattern is None


def test_extract_class_pattern_too_few_classes() -> None:
    entries = [_file_entry("a.py", "python", [_sym_dict("OnlyOne", "class")])]
    pattern = _extract_class_pattern(entries)
    assert pattern is None


# ── _is_dto_directory ─────────────────────────────────────────────────────────

def _dto_entry(file: str, class_name: str) -> dict:
    """A file entry with one class and no methods — canonical DTO shape."""
    return {"file": file, "language": "c_sharp", "symbols": [
        {"name": class_name, "kind": "class", "signature": f"public class {class_name}", "methods": []}
    ]}


def test_is_dto_directory_pure_dtos() -> None:
    entries = [_dto_entry(f"Entities/Dto{i}.cs", f"Dto{i}") for i in range(8)]
    assert _is_dto_directory(entries)


def test_is_dto_directory_not_dtos_with_methods() -> None:
    entries = [
        {"file": f"src/svc{i}.cs", "language": "c_sharp", "symbols": [
            {"name": f"Service{i}", "kind": "class", "methods": ["Get", "Save"]}
        ]}
        for i in range(8)
    ]
    assert not _is_dto_directory(entries)


def test_is_dto_directory_mixed_mostly_dtos() -> None:
    """80% DTOs + 20% with methods → still a DTO directory."""
    entries = [_dto_entry(f"Entities/Dto{i}.cs", f"Dto{i}") for i in range(8)]
    entries.append({"file": "Entities/Special.cs", "language": "c_sharp", "symbols": [
        {"name": "Special", "kind": "class", "methods": ["Compute"]}
    ]})
    entries.append({"file": "Entities/Other.cs", "language": "c_sharp", "symbols": [
        {"name": "Other", "kind": "class", "methods": ["Run"]}
    ]})
    # 8/10 = 80% → exactly at threshold
    assert _is_dto_directory(entries)


def test_is_dto_directory_mixed_too_many_with_methods() -> None:
    """Less than 80% DTOs → not a DTO directory."""
    entries = [_dto_entry(f"Entities/Dto{i}.cs", f"Dto{i}") for i in range(6)]
    for i in range(4):
        entries.append({"file": f"src/svc{i}.cs", "language": "c_sharp", "symbols": [
            {"name": f"Service{i}", "kind": "class", "methods": ["Get"]}
        ]})
    # 6/10 = 60% → below threshold
    assert not _is_dto_directory(entries)


def test_is_dto_directory_empty_entries() -> None:
    assert not _is_dto_directory([])


# ── _aggregate_by_directory: DTO directories ─────────────────────────────────

def test_aggregate_dto_directory_produces_summary() -> None:
    """A directory of DTO classes (no methods) above threshold → directory_summary."""
    entries = [_dto_entry(f"Entities/Dto{i}.cs", f"OrderDto{i}") for i in range(8)]
    result = _aggregate_by_directory(entries, threshold=8, profile=_MEDIUM)
    assert len(result) == 1
    summary = result[0]
    assert summary["kind"] == "directory_summary"
    assert summary["file_count"] == 8
    assert summary["note"] == "DTO/entity classes — data containers with no methods"


def test_aggregate_dto_directory_includes_naming_pattern() -> None:
    entries = [_dto_entry(f"Entities/f{i}.cs", f"OrderDto{i}") for i in range(8)]
    result = _aggregate_by_directory(entries, threshold=8, profile=_MEDIUM)
    summary = result[0]
    assert "naming_pattern" in summary
    assert "Dto" in summary["naming_pattern"]["pattern"]
    assert summary["naming_pattern"]["total"] == 8


def test_aggregate_dto_directory_includes_sample_files() -> None:
    entries = [_dto_entry(f"Entities/Dto{i}.cs", f"Dto{i}") for i in range(8)]
    result = _aggregate_by_directory(entries, threshold=8, profile=_MEDIUM)
    assert "sample_files" in result[0]
    assert len(result[0]["sample_files"]) <= 3


def test_aggregate_dto_below_threshold_keeps_individual() -> None:
    """DTO directory below threshold → individual files, no summary."""
    entries = [_dto_entry(f"Entities/Dto{i}.cs", f"Dto{i}") for i in range(4)]
    result = _aggregate_by_directory(entries, threshold=8, profile=_MEDIUM)
    assert all(e.get("kind") != "directory_summary" for e in result)


# ── _detect_wiring: route detection ──────────────────────────────────────────

def _make_analysis(path: str, language: str, symbols: list[SymbolInfo]) -> FileAnalysis:
    return FileAnalysis(path=path, language=language, symbols=symbols)


def test_wiring_csharp_routes() -> None:
    """C# controller with [Route] + [HttpGet/Post] → route_map."""
    symbols = [
        SymbolInfo(name="OrderController", kind="class", decorators=['ApiController', 'Route("api/orders")']),
        SymbolInfo(name="GetAll", kind="method", parent="OrderController", decorators=["HttpGet"]),
        SymbolInfo(name="Create", kind="method", parent="OrderController", decorators=["HttpPost"]),
        SymbolInfo(name="GetById", kind="method", parent="OrderController", decorators=['HttpGet("{id}")']),
    ]
    analyses = {"Controllers/OrderController.cs": _make_analysis("Controllers/OrderController.cs", "c_sharp", symbols)}
    result = _detect_wiring(analyses, _MEDIUM)

    assert "route_map" in result
    entry = result["route_map"][0]
    assert entry["class"] == "OrderController"
    routes = entry["routes"]
    methods = {r["method"] for r in routes}
    assert "GET" in methods
    assert "POST" in methods
    assert any("api/orders" in r["path"] for r in routes)


def test_wiring_typescript_nestjs_routes() -> None:
    """NestJS @Controller + @Get/@Post → route_map."""
    symbols = [
        SymbolInfo(name="UserController", kind="class", decorators=["Controller('/api/users')"]),
        SymbolInfo(name="findAll", kind="method", parent="UserController", decorators=["Get()"]),
        SymbolInfo(name="create", kind="method", parent="UserController", decorators=["Post()"]),
        SymbolInfo(name="findOne", kind="method", parent="UserController", decorators=["Get(':id')"]),
    ]
    analyses = {"src/user.controller.ts": _make_analysis("src/user.controller.ts", "typescript", symbols)}
    result = _detect_wiring(analyses, _MEDIUM)

    assert "route_map" in result
    entry = result["route_map"][0]
    assert entry["class"] == "UserController"
    methods = {r["method"] for r in entry["routes"]}
    assert "GET" in methods
    assert "POST" in methods


def test_wiring_python_fastapi_routes() -> None:
    """FastAPI @app.get/@router.post → route_map."""
    symbols = [
        SymbolInfo(name="list_users", kind="function", decorators=["app.get('/users')"]),
        SymbolInfo(name="create_user", kind="function", decorators=["app.post('/users')"]),
        SymbolInfo(name="get_user", kind="function", decorators=["app.get('/users/{id}')"]),
    ]
    analyses = {"src/routes.py": _make_analysis("src/routes.py", "python", symbols)}
    result = _detect_wiring(analyses, _MEDIUM)

    assert "route_map" in result
    entry = result["route_map"][0]
    assert entry["file"] == "src/routes.py"
    methods = {r["method"] for r in entry["routes"]}
    assert "GET" in methods
    assert "POST" in methods
    assert any("/users" in r["path"] for r in entry["routes"])


def test_wiring_go_handlers() -> None:
    """Go functions with http.ResponseWriter or *gin.Context → handlers."""
    symbols = [
        SymbolInfo(name="HealthCheck", kind="function", signature="func HealthCheck(w http.ResponseWriter, r *http.Request)"),
        SymbolInfo(name="GetOrder", kind="method", parent="OrderHandler", signature="func (h *OrderHandler) GetOrder(c *gin.Context)"),
        SymbolInfo(name="helperFunc", kind="function", signature="func helperFunc(x int)"),
    ]
    analyses = {"handlers.go": _make_analysis("handlers.go", "go", symbols)}
    result = _detect_wiring(analyses, _MEDIUM)

    assert "route_map" in result
    handlers = result["route_map"][0]["handlers"]
    handler_names = {h["handler"] for h in handlers}
    assert "HealthCheck" in handler_names
    assert "GetOrder" in handler_names
    assert "helperFunc" not in handler_names


def test_wiring_no_routes_returns_empty() -> None:
    """Files with no route indicators → empty result."""
    symbols = [
        SymbolInfo(name="UserService", kind="class"),
        SymbolInfo(name="getUser", kind="method", parent="UserService"),
    ]
    analyses = {"src/service.ts": _make_analysis("src/service.ts", "typescript", symbols)}
    result = _detect_wiring(analyses, _MEDIUM)
    assert result == {}


# ── _cap_routes ──────────────────────────────────────────────────────────────

def test_cap_routes_below_limit_unchanged() -> None:
    routes = [
        {"method": "GET", "path": "/a", "handler": "a"},
        {"method": "POST", "path": "/b", "handler": "b"},
    ]
    assert _cap_routes(routes, 5) == routes


def test_cap_routes_one_per_method_first() -> None:
    """With 8 routes (5 GET, 2 POST, 1 DELETE) and cap=5, get one of each method + extras."""
    routes = [
        {"method": "GET", "path": "/g1", "handler": "g1"},
        {"method": "GET", "path": "/g2", "handler": "g2"},
        {"method": "GET", "path": "/g3", "handler": "g3"},
        {"method": "GET", "path": "/g4", "handler": "g4"},
        {"method": "GET", "path": "/g5", "handler": "g5"},
        {"method": "POST", "path": "/p1", "handler": "p1"},
        {"method": "POST", "path": "/p2", "handler": "p2"},
        {"method": "DELETE", "path": "/d1", "handler": "d1"},
    ]
    result = _cap_routes(routes, 5)
    assert len(result) == 5

    methods = {r["method"] for r in result}
    # All three method types must be represented
    assert "GET" in methods
    assert "POST" in methods
    assert "DELETE" in methods


def test_cap_routes_fills_remaining_slots() -> None:
    """When only 2 method types exist, remaining slots go to extras."""
    routes = [
        {"method": "GET", "path": "/g1", "handler": "g1"},
        {"method": "GET", "path": "/g2", "handler": "g2"},
        {"method": "GET", "path": "/g3", "handler": "g3"},
        {"method": "POST", "path": "/p1", "handler": "p1"},
    ]
    result = _cap_routes(routes, 3)
    assert len(result) == 3
    methods = {r["method"] for r in result}
    assert "GET" in methods
    assert "POST" in methods


def test_wiring_caps_controllers() -> None:
    """More controllers than profile cap → capped with total_route_files count."""
    analyses = {}
    for i in range(20):
        symbols = [
            SymbolInfo(name=f"Ctrl{i}", kind="class", decorators=["Controller()"]),
            SymbolInfo(name="index", kind="method", parent=f"Ctrl{i}", decorators=["Get()"]),
        ]
        analyses[f"ctrl{i}.ts"] = _make_analysis(f"ctrl{i}.ts", "typescript", symbols)

    result = _detect_wiring(analyses, _MEDIUM)
    assert len(result["route_map"]) == _MEDIUM.max_route_controllers
    assert result["total_route_files"] == 20


def test_wiring_caps_routes_per_controller() -> None:
    """Controller with many routes → capped per profile with total_routes count."""
    symbols = [SymbolInfo(name="BigCtrl", kind="class", decorators=["Controller('/api')"])]
    for i in range(15):
        method = ["Get", "Post", "Put", "Delete", "Patch"][i % 5]
        symbols.append(SymbolInfo(
            name=f"action{i}", kind="method", parent="BigCtrl",
            decorators=[f"{method}('/r{i}')"],
        ))
    analyses = {"big.ts": _make_analysis("big.ts", "typescript", symbols)}
    result = _detect_wiring(analyses, _MEDIUM)

    entry = result["route_map"][0]
    assert len(entry["routes"]) == _MEDIUM.max_routes_per_controller
    assert entry["total_routes"] == 15
    # All method types should be represented
    methods = {r["method"] for r in entry["routes"]}
    assert len(methods) == 5


# ── _build_interface_impl_map ────────────────────────────────────────────────

def test_interface_impl_map_basic() -> None:
    """Interfaces with known implementors produce a map."""
    analyses = {
        "ifaces.ts": FileAnalysis(path="ifaces.ts", language="typescript", symbols=[
            SymbolInfo(name="IUserService", kind="interface"),
            SymbolInfo(name="Serializable", kind="interface"),
        ]),
        "impl.ts": FileAnalysis(path="impl.ts", language="typescript", symbols=[
            SymbolInfo(name="UserService", kind="class", implements=["IUserService", "Serializable"]),
            SymbolInfo(name="AdminService", kind="class", implements=["IUserService"]),
        ]),
    }
    result = _build_interface_impl_map(analyses)
    assert "IUserService" in result
    assert "UserService" in result["IUserService"]
    assert "AdminService" in result["IUserService"]
    assert "UserService" in result["Serializable"]


def test_interface_impl_map_ignores_unknown_interfaces() -> None:
    """Classes implementing interfaces not in the project are excluded."""
    analyses = {
        "impl.ts": FileAnalysis(path="impl.ts", language="typescript", symbols=[
            SymbolInfo(name="MyClass", kind="class", implements=["ExternalInterface"]),
        ]),
    }
    result = _build_interface_impl_map(analyses)
    assert result == {}


def test_interface_impl_map_go_convention() -> None:
    """Go I{Name} convention matches to {Name} struct."""
    analyses = {
        "repo.go": FileAnalysis(path="repo.go", language="go", symbols=[
            SymbolInfo(name="IOrderRepo", kind="interface"),
            SymbolInfo(name="OrderRepo", kind="struct"),
        ]),
    }
    result = _build_interface_impl_map(analyses)
    assert "IOrderRepo" in result
    assert "OrderRepo" in result["IOrderRepo"]


def test_interface_impl_map_empty_when_no_interfaces() -> None:
    analyses = {
        "svc.ts": FileAnalysis(path="svc.ts", language="typescript", symbols=[
            SymbolInfo(name="UserService", kind="class"),
        ]),
    }
    result = _build_interface_impl_map(analyses)
    assert result == {}
