orchestrata_tools.test_generator.testgen.structural

Reference › orchestrata_tools › test_generator › testgen › structural

This module is the mechanical Layer 2 generator. It writes high-confidence tests from purely structural facts: dataclass field metadata, enum members, abstract method lists and protocol contracts. Each function returns a rendered pytest class or function block ready to be spliced into a TestFile from writer. It is imported by generator and the package, and covered by tests/test_orchestrata_tools_testgen_structural_coverage.py.

Every function takes a class name, an "extra" dictionary of structural facts built by adapter, an import path, an optional TestFile and a config: gen_dataclass_tests, gen_enum_tests, gen_abstract_tests and gen_protocol_tests. gen_entry_point_tests takes the top N ranked entries from ranking and writes a prioritised stub class. Two template fixes are recorded: an unresolved type emits cast(Any, None) and registers the typing import in the same file, and a deliberately invalid constructor call inside pytest.raises(TypeError) carries a pyright ignore comment.

At a glance

Source src/orchestrata_tools/test_generator/testgen/structural.py
Lines of code 774
Classes 0
Functions 9
Imports (internal) orchestrata_tools.test_generator.testgen.config, orchestrata_tools.test_generator.testgen.ranking, orchestrata_tools.test_generator.testgen.types, orchestrata_tools.test_generator.testgen.writer
Imported by orchestrata_tools.test_generator.testgen, orchestrata_tools.test_generator.testgen.generator
Tests that import it tests/test_orchestrata_tools_testgen_structural_coverage.py

Class diagram

No classes; see Functions.

Dependency diagram

Dependency diagram orchestrata_tools.test_generator.testgen.structural

API

Functions

Function Signature Summary
gen_dataclass_tests gen_dataclass_tests(class_name: str, extra: dict[str, Any], import_path: str, tf: TestFile | None = None, config: ProjectConfig | None = None) -> str Generate a pytest class with tests for a @dataclass.
gen_enum_tests gen_enum_tests(class_name: str, extra: dict[str, Any], import_path: str, tf: TestFile | None = None, config: ProjectConfig | None = None) -> str Generate a pytest class with tests for an Enum subclass.
gen_abstract_tests gen_abstract_tests(class_name: str, extra: dict[str, Any], import_path: str, tf: TestFile | None = None, config: ProjectConfig | None = None) -> str Generate a pytest class with tests for an ABC.
_method_stub _method_stub(m: dict) -> str Return a sensible stub return value for a method.
gen_protocol_tests gen_protocol_tests(class_name: str, extra: dict[str, Any], import_path: str, tf: TestFile | None = None, config: ProjectConfig | None = None) -> str Generate: 1.
gen_entry_point_tests gen_entry_point_tests(ranked_entries: list[RankedEntry], top_n: int = 10) -> str For the top-N ranked functions, generate a prioritised TODO stub class with a comment showing priority/score and a basic call stub.
_field_sentinel _field_sentinel(field_info: dict, tf: TestFile | None = None, config: ProjectConfig | None = None) -> str Return a sentinel value for a dataclass field, tracking imports.
_sentinel_for_annotation _sentinel_for_annotation(annotation: str | None, tf: TestFile | None = None, config: ProjectConfig | None = None) -> str Return a sentinel value for a type annotation, tracking imports.
_sig_from_params _sig_from_params(params: list[Any]) -> str Build a call-site argument string from a list of ParamInfo-like objects (orchestrata_ast.models.ParamInfo: .name, .type_annotation, .kind).

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_tools/test_generator/testgen/structural.py

"""structural.py — Layer 2 mechanical test generation.

Generates high-confidence, zero-hallucination tests from purely structural
information: dataclass field metadata, enum members, abstract method lists,
and protocol member contracts.

Each generator returns a rendered string (a pytest class or standalone
function block) ready to be spliced into a TestFile.

Restored 2026-08-19 from ``scripts/ast_serializer/testgen`` (deleted
2026-07-09, commit 6a186c9d). This module was already self-contained (only
``.config``/``.types``/``.writer``, no dependency on the deleted parser's
``StatementRecord`` tree — every public function here takes a plain
``dict[str, Any]`` "extra" bag, not a parser type) — the caller
(``orchestrata_tools.test_generator.testgen.adapter``) builds that dict
from the current AST layer (``orchestrata_ast.models``) plus a direct,
narrow walk of the already-parsed ``ast.ClassDef`` node for the handful of
facts (dataclass field defaults, enum member values) the current AST layer
doesn't carry. Only the imports moved to absolute paths.

Template-defect fixes applied here:
  (b) sentinel() may now emit ``cast(Any, None)`` for an unresolved type;
      every call site below registers the needed typing import on the
      TestFile in the same place it emits the cast.
  (d) a deliberately-invalid constructor call inside a
      ``pytest.raises(TypeError)`` block (missing required dataclass
      field / abstract class direct instantiation) carries
      ``# pyright: ignore[reportCallIssue]`` — these calls are invalid by
      construction, that's the point of the test, and pyright's static
      check has no way to know that.

Also drops the "mocked variant" half of ``gen_entry_point_tests``: this
project's hard rule is "no mocking, ever" (the only sanctioned test double
is the LLM boundary), so ``@mock.patch``/``MagicMock`` scaffolding had no
legitimate use here. The basic TODO stub half is kept, gated by
``ProjectConfig.enable_entry_point_stubs`` (renamed from the old
``enable_mock_tests`` gate — same default, off, for ORCHESTRATA_ULTIMATE).
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from orchestrata_tools.test_generator.testgen.types import (
    sentinel,
    sentinel_imports,
    sentinel_needs_cast,
)
from orchestrata_tools.test_generator.testgen.writer import CodeBuilder, TestFile

if TYPE_CHECKING:
    from orchestrata_tools.test_generator.testgen.config import ProjectConfig
    from orchestrata_tools.test_generator.testgen.ranking import RankedEntry

# ---------------------------------------------------------------------------
# Dataclass tests
# ---------------------------------------------------------------------------


def gen_dataclass_tests(
    class_name: str,
    extra: dict[str, Any],
    import_path: str,
    tf: TestFile | None = None,
    config: ProjectConfig | None = None,
) -> str:
    """
    Generate a pytest class with tests for a @dataclass.

    Covers:
      - Required fields enforced (TypeError when missing)
      - Default values are correct
      - All fields assignable / readable
      - frozen=True → FrozenInstanceError on assignment
      - slots=True  → no __dict__
      - eq=True     → equality by value (default)
    """
    fields: list[dict] = extra.get("dataclass_fields", [])
    params: dict = extra.get("dataclass_params", {})
    frozen = params.get("frozen") == "True"
    slots = params.get("slots") == "True"
    eq = params.get("eq", "True") != "False"  # default True
    has_post_init = extra.get("has_post_init", False)

    required = [f for f in fields if f["required"]]
    optional = [f for f in fields if not f["required"]]

    cb = CodeBuilder()
    cb.cls(f"Test{class_name}Dataclass")
    cb.docstring(
        f"Structural tests for @dataclass {class_name}.\n"
        f"Fields: {[f['name'] for f in fields]}"
    )
    cb.blank()

    # ---- minimal valid instance helper ----
    all_args = ", ".join(
        f"{f['name']}={_field_sentinel(f, tf, config)}" for f in required
    )
    cb.comment("Helper: minimal valid instance")
    cb.func("_make")
    cb.line(f"return {class_name}({all_args})")
    cb.end()
    cb.blank()

    # ---- required fields ----
    if required:
        cb.func("test_required_fields_enforced")
        cb.docstring("Omitting any required field must raise TypeError.")
        cb.pytest_raises("TypeError")
        cb.line(
            f"{class_name}()  # missing: {', '.join(f['name'] for f in required)}"
            "  # pyright: ignore[reportCallIssue]"
        )
        cb.end()  # with
        cb.end()  # def
        cb.blank()

        for f in required:
            # Drop one required field at a time
            others = [r for r in required if r["name"] != f["name"]]
            args = ", ".join(
                f"{r['name']}={_field_sentinel(r, tf, config)}" for r in others
            )
            cb.func(f"test_missing_{f['name']}_raises")
            cb.pytest_raises("TypeError")
            call = f"{class_name}({args})" if args else f"{class_name}()"
            cb.line(
                f"{call}  # missing: {f['name']}  # pyright: ignore[reportCallIssue]"
            )
            cb.end()
            cb.end()
            cb.blank()

    # ---- defaults ----
    # Skip exact default assertions when __post_init__ may mutate fields
    if optional and not has_post_init:
        cb.func("test_default_values")
        cb.docstring("Default values must match the class definition.")
        if all_args:
            cb.line(f"obj = {class_name}({all_args})")
        else:
            cb.line(f"obj = {class_name}()")
        for f in optional:
            if f["default"] is not None:
                cb.line(f"assert obj.{f['name']} == {f['default']}")
            elif f["default_factory"] is not None:
                # Default factories may reference modules not imported in the
                # test file; assert non-None and correct type instead.
                cb.line(f"assert obj.{f['name']} is not None")
        cb.end()
        cb.blank()

    # ---- full construction / field readback ----
    all_fields_args = ", ".join(
        f"{f['name']}={_field_sentinel(f, tf, config)}" for f in fields
    )
    cb.func("test_field_assignment_and_readback")
    cb.line(f"obj = {class_name}({all_fields_args})")
    for f in fields:
        sval = _field_sentinel(f, tf, config)
        # For callable sentinels, use callable() check instead of equality
        if "lambda" in sval:
            cb.line(f"assert callable(obj.{f['name']})")
        else:
            cb.line(f"assert obj.{f['name']} == {sval}")
    cb.end()
    cb.blank()

    # ---- frozen ----
    if frozen:
        cb.func("test_frozen_immutability")
        cb.docstring(
            "frozen=True: attribute assignment must raise FrozenInstanceError."
        )
        cb.line("from dataclasses import FrozenInstanceError")
        if all_args:
            cb.line(f"obj = {class_name}({all_args})")
        else:
            cb.line(f"obj = {class_name}()")
        if fields:
            first = fields[0]
            cb.pytest_raises("FrozenInstanceError")
            cb.line(f"obj.{first['name']} = {_field_sentinel(first, tf, config)}")
            cb.end()
        cb.end()
        cb.blank()

    # ---- slots ----
    if slots:
        cb.func("test_slots_no_dict")
        cb.docstring("slots=True: instances must not have a __dict__.")
        cb.line("obj = self._make()")
        cb.line("assert not hasattr(obj, '__dict__')")
        cb.end()
        cb.blank()

    # ---- equality (eq=True by default) ----
    if eq and fields:
        cb.func("test_equality_by_value")
        cb.line(f"a = {class_name}({all_fields_args})")
        cb.line(f"b = {class_name}({all_fields_args})")
        cb.line("assert a == b")
        cb.line("assert a is not b")
        cb.end()
        cb.blank()

    # ---- field() metadata ----
    for f in fields:
        if f.get("is_field_call") and f.get("metadata") and f["metadata"] != "None":
            cb.func(f"test_{f['name']}_field_metadata")
            cb.docstring(f"field() metadata for {f['name']}: {f['metadata']}")
            cb.line("import dataclasses")
            cb.line(f"field_info = dataclasses.fields({class_name})")
            cb.line("meta = {f.name: f.metadata for f in field_info}")
            cb.line(f"assert '{f['name']}' in meta")
            cb.end()
            cb.blank()

    cb.end()  # class
    return cb.render()


# ---------------------------------------------------------------------------
# Enum tests
# ---------------------------------------------------------------------------


def gen_enum_tests(
    class_name: str,
    extra: dict[str, Any],
    import_path: str,
    tf: TestFile | None = None,
    config: ProjectConfig | None = None,
) -> str:
    """
    Generate a pytest class with tests for an Enum subclass.

    Covers:
      - All members exist by name
      - Known values are correct (skips auto() members)
      - Lookup by value works
      - Invalid value raises ValueError
      - Member count matches definition
      - Iteration yields all members
      - String representation is stable
    """
    members: list[dict] = extra.get("enum_members", [])
    base = extra.get("enum_base", "Enum")

    cb = CodeBuilder()
    cb.cls(f"Test{class_name}Enum")
    cb.docstring(
        f"Structural tests for {base} subclass {class_name}.\n"
        f"Members: {[m['name'] for m in members]}"
    )
    cb.blank()

    # ---- member existence ----
    cb.func("test_all_members_exist")
    cb.docstring("Every declared member must be accessible by name.")
    for m in members:
        cb.line(f"assert hasattr({class_name}, '{m['name']}')")
        cb.line(f"assert {class_name}.{m['name']} is not None")
    cb.end()
    cb.blank()

    # ---- known values (skip auto) ----
    known = [m for m in members if not m["is_auto"]]
    if known:
        cb.func("test_member_values")
        cb.docstring("Declared values must match the definition exactly.")
        for m in known:
            cb.line(f"assert {class_name}.{m['name']}.value == {m['value_repr']}")
        cb.end()
        cb.blank()

    # ---- lookup by value ----
    if known:
        cb.func("test_lookup_by_value")
        for m in known[:3]:  # first 3 to keep tests concise
            cb.line(
                f"assert {class_name}({m['value_repr']}) is {class_name}.{m['name']}"
            )
        cb.end()
        cb.blank()

    # ---- invalid value ----
    cb.func("test_invalid_value_raises")
    cb.pytest_raises("ValueError")
    if base == "IntEnum":
        cb.line(f"{class_name}(999999)")
    elif base == "StrEnum":
        cb.line(f'{class_name}("__invalid__")')
    else:
        cb.line(f"{class_name}(object())  # no member matches")
    cb.end()
    cb.end()
    cb.blank()

    # ---- member count ----
    cb.func("test_member_count")
    cb.line(f"assert len({class_name}) == {len(members)}")
    cb.end()
    cb.blank()

    # ---- iteration ----
    cb.func("test_iteration_yields_all_members")
    cb.line(f"all_members = list({class_name})")
    cb.line(f"assert len(all_members) == {len(members)}")
    for m in members:
        cb.line(f"assert {class_name}.{m['name']} in all_members")
    cb.end()
    cb.blank()

    # ---- containment / membership ----
    if members:
        first = members[0]
        cb.func("test_membership")
        cb.line(f"assert {class_name}.{first['name']} in {class_name}")
        cb.end()
        cb.blank()

    # ---- string repr stability ----
    cb.func("test_repr_is_stable")
    cb.docstring("repr() should not raise and should include the class name.")
    if members:
        m = members[0]
        cb.line(f"r = repr({class_name}.{m['name']})")
        cb.line(f"assert '{class_name}' in r")
    cb.end()
    cb.blank()

    # ---- name attribute ----
    if members:
        m = members[0]
        cb.func("test_name_attribute")
        cb.line(f"assert {class_name}.{m['name']}.name == '{m['name']}'")
        cb.end()
        cb.blank()

    # ---- IntEnum arithmetic (if applicable) ----
    if base == "IntEnum" and known:
        cb.func("test_int_coercion")
        cb.docstring("IntEnum members must behave as integers.")
        m = known[0]
        cb.line(f"val = int({class_name}.{m['name']})")
        cb.line(f"assert val == {m['value_repr']}")
        cb.end()
        cb.blank()

    cb.end()  # class
    return cb.render()


# ---------------------------------------------------------------------------
# Abstract class tests
# ---------------------------------------------------------------------------


def gen_abstract_tests(
    class_name: str,
    extra: dict[str, Any],
    import_path: str,
    tf: TestFile | None = None,
    config: ProjectConfig | None = None,
) -> str:
    """
    Generate a pytest class with tests for an ABC.

    Covers:
      - Direct instantiation raises TypeError
      - Partial implementation raises TypeError
      - Full concrete subclass can be instantiated
      - Each abstract method is callable on the concrete subclass
    """
    abstract_methods: list[dict] = extra.get("abstract_methods", [])
    abc_base = extra.get("abc_base", "ABC")

    cb = CodeBuilder()
    cb.cls(f"Test{class_name}Abstract")
    cb.docstring(
        f"Structural tests for abstract class {class_name} ({abc_base}).\n"
        f"Abstract methods: {[m['name'] for m in abstract_methods]}"
    )
    cb.blank()

    # ---- cannot instantiate directly ----
    cb.func("test_cannot_instantiate_directly")
    cb.docstring(
        f"{class_name} is abstract and must raise TypeError on direct instantiation."
    )
    cb.pytest_raises("TypeError")
    cb.line(f"{class_name}()  # pyright: ignore[reportCallIssue]")
    cb.end()
    cb.end()
    cb.blank()

    # ---- partial implementation fails ----
    if len(abstract_methods) > 1:
        for missing in abstract_methods[:2]:  # first 2 to avoid bloat
            others = [m for m in abstract_methods if m["name"] != missing["name"]]
            cb.func(f"test_missing_{missing['name']}_raises")
            cb.docstring(
                f"Omitting {missing['name']} from the subclass must still raise TypeError."
            )
            cb.cls("_Partial", [class_name])
            for m in others:
                body = _method_stub(m)
                cb.func(m["name"], "self, *args, **kwargs")
                cb.line(body)
                cb.end()
            cb.end()  # _Partial class
            cb.pytest_raises("TypeError")
            cb.line("_Partial()  # pyright: ignore[reportCallIssue]")
            cb.end()
            cb.end()
            cb.blank()

    # ---- full concrete subclass ----
    cb.func("test_concrete_subclass_instantiates")
    cb.docstring(
        "A class implementing all abstract methods must instantiate without error."
    )
    cb.cls("_Concrete", [class_name])
    for m in abstract_methods:
        body = _method_stub(m)
        if m.get("is_property"):
            cb.line("@property")
            cb.func(m["name"], "self")
        else:
            cb.func(m["name"], "self, *args, **kwargs")
        cb.line(body)
        cb.end()
    cb.end()  # _Concrete class
    cb.line("obj = _Concrete()")
    cb.line("assert obj is not None")
    cb.end()
    cb.blank()

    # ---- each abstract method is callable ----
    if abstract_methods:
        cb.func("test_abstract_methods_callable_on_concrete")
        cb.cls("_Concrete", [class_name])
        for m in abstract_methods:
            body = _method_stub(m)
            if m.get("is_property"):
                cb.line("@property")
                cb.func(m["name"], "self")
            else:
                cb.func(m["name"], "self, *args, **kwargs")
            cb.line(body)
            cb.end()
        cb.end()  # _Concrete
        cb.line("obj = _Concrete()")
        for m in abstract_methods:
            if m.get("is_property"):
                cb.line(f"_ = obj.{m['name']}")
                cb.line(f"assert True  # property accessible: {m['name']}")
            else:
                cb.line(f"result = obj.{m['name']}()")
                cb.line(f"assert True  # method callable: {m['name']}")
        cb.end()
        cb.blank()

    cb.end()  # class
    return cb.render()


def _method_stub(m: dict) -> str:
    """Return a sensible stub return value for a method."""
    ret = m.get("signature") or ""
    if "-> None" in ret or not ret:
        return "pass"
    if "-> bool" in ret:
        return "return False"
    if "-> int" in ret:
        return "return 0"
    if "-> float" in ret:
        return "return 0.0"
    if "-> str" in ret:
        return 'return ""'
    if "-> list" in ret.lower():
        return "return []"
    if "-> dict" in ret.lower():
        return "return {}"
    return "return None  # TODO"


# ---------------------------------------------------------------------------
# Protocol tests
# ---------------------------------------------------------------------------


def gen_protocol_tests(
    class_name: str,
    extra: dict[str, Any],
    import_path: str,
    tf: TestFile | None = None,
    config: ProjectConfig | None = None,
) -> str:
    """
    Generate:
      1. A standalone conformance-checker function (usable in other tests).
      2. A pytest class testing the protocol contract.

    Covers:
      - @runtime_checkable isinstance check (if applicable)
      - Non-conforming object fails isinstance
      - Minimal conforming class passes the checker
      - Each required attribute/method is validated
    """
    members: list[dict] = extra.get("protocol_members", [])
    checkable: bool = extra.get("is_runtime_checkable", False)

    attrs = [m for m in members if m["kind"] == "attribute"]
    methods = [m for m in members if m["kind"] in ("method", "async_method")]

    cb = CodeBuilder()

    # ---- standalone conformance checker ----
    cb.comment("Protocol conformance checker — import and use in integration tests")
    cb.func(f"assert_{class_name.lower()}_conformance", args="obj: object")
    cb.docstring(
        f"Assert that obj satisfies the {class_name} protocol.\n\n"
        f"Required attributes: {[a['name'] for a in attrs]}\n"
        f"Required methods:    {[m['name'] for m in methods]}"
    )
    for a in attrs:
        ann = f" ({a['type']})" if a.get("type") else ""
        cb.line(
            f'assert hasattr(obj, "{a["name"]}"), '
            f'"Missing {class_name} attribute: {a["name"]}{ann}"'
        )
    for m in methods:
        if m.get("is_property"):
            cb.line(
                f'assert hasattr(obj, "{m["name"]}"), '
                f'"Missing {class_name} property: {m["name"]}"'
            )
        else:
            cb.line(
                f'assert callable(getattr(obj, "{m["name"]}", None)), '
                f'"Missing {class_name} method: {m["name"]}"'
            )
    cb.end()
    cb.blank()
    cb.blank()

    # ---- pytest class ----
    cb.cls(f"Test{class_name}Protocol")
    cb.docstring(
        f"Structural tests for Protocol {class_name}.\n"
        + ("@runtime_checkable" if checkable else "Not @runtime_checkable")
    )
    cb.blank()

    # build a minimal conforming class inline
    def _add_conforming_class() -> None:
        cb.comment("Minimal conforming implementation")
        cb.cls("_Conforming")
        if not attrs and not methods:
            cb.line("pass")
        for a in attrs:
            cb.line(
                f"{a['name']}: {a['type'] or 'object'} = "
                f"{_sentinel_for_annotation(a.get('type'), tf, config)}"
            )
        for m in methods:
            if m.get("is_property"):
                cb.line("@property")
                cb.func(m["name"], "self")
                cb.line(
                    f"return {_sentinel_for_annotation(m.get('returns'), tf, config)}"
                )
                cb.end()
            elif m.get("is_classmethod"):
                cb.line("@classmethod")
                cb.func(m["name"], "cls, *args, **kwargs")
                cb.line("return cls()")
                cb.end()
            elif m.get("is_staticmethod"):
                cb.line("@staticmethod")
                cb.func(m["name"], "*args, **kwargs")
                cb.line("return None")
                cb.end()
            else:
                cb.func(m["name"], "self, *args, **kwargs")
                cb.line(
                    f"return {_sentinel_for_annotation(m.get('returns'), tf, config)}"
                )
                cb.end()
        cb.end()  # _Conforming

    # ---- runtime_checkable isinstance ----
    if checkable:
        cb.func("test_conforming_isinstance")
        cb.docstring(
            f"@runtime_checkable: isinstance(obj, {class_name}) must return True "
            f"for a conforming object."
        )
        _add_conforming_class()
        cb.line("obj = _Conforming()")
        cb.line(f"assert isinstance(obj, {class_name})")
        cb.end()
        cb.blank()

        cb.func("test_non_conforming_isinstance")
        cb.docstring("A plain object() must not satisfy the protocol.")
        cb.line(f"assert not isinstance(object(), {class_name})")
        cb.end()
        cb.blank()

    # ---- conformance checker ----
    cb.func("test_conformance_checker_passes")
    cb.docstring("The standalone conformance checker must not raise for a valid impl.")
    _add_conforming_class()
    cb.line("obj = _Conforming()")
    cb.line(f"assert_{class_name.lower()}_conformance(obj)  # must not raise")
    cb.end()
    cb.blank()

    cb.func("test_conformance_checker_catches_missing_members")
    cb.docstring(
        "The conformance checker must raise AssertionError for an empty class."
    )
    if attrs or methods:
        cb.pytest_raises("AssertionError")
        cb.line(f"assert_{class_name.lower()}_conformance(object())")
        cb.end()
    else:
        cb.line("pass  # no members to check")
    cb.end()
    cb.blank()

    # ---- per-attribute existence ----
    if attrs:
        cb.func("test_required_attributes_present")
        _add_conforming_class()
        cb.line("obj = _Conforming()")
        for a in attrs:
            cb.line(
                f'assert hasattr(obj, "{a["name"]}"), '
                f'"expected attribute {a["name"]}"'
            )
        cb.end()
        cb.blank()

    # ---- per-method callability ----
    if methods:
        cb.func("test_required_methods_callable")
        _add_conforming_class()
        cb.line("obj = _Conforming()")
        for m in [x for x in methods if not x.get("is_property")]:
            cb.line(f'assert callable(getattr(obj, "{m["name"]}", None))')
        cb.end()
        cb.blank()

    cb.end()  # class
    return cb.render()


# ---------------------------------------------------------------------------
# Entry-point ranked stubs (Layer 1)
# ---------------------------------------------------------------------------


def gen_entry_point_tests(ranked_entries: list[RankedEntry], top_n: int = 10) -> str:
    """
    For the top-N ranked functions, generate a prioritised TODO stub class
    with a comment showing priority/score and a basic call stub.

    No mocked-dependency variant: this project's hard rule is "no
    mocking, ever" — a real integration test belongs in a hand-written
    suite, not a generated one that can't know which collaborators are
    safe to exercise for real.
    """
    cb = CodeBuilder()
    cb.comment("=" * 70)
    cb.comment("Entry point tests — ranked by fan_in + reachable scope")
    cb.comment("=" * 70)
    cb.blank()

    for entry in ranked_entries[:top_n]:
        short = entry.symbol.name

        sig_args = _sig_from_params(entry.symbol.parameters)
        call_str = (
            f"{short}({sig_args})"
            if not entry.is_method
            else f"obj.{short}({sig_args})"
        )

        cls_name = (
            f"Test{entry.class_name}{short.title().replace('_', '')}"
            if entry.is_method
            else f"Test{short.title().replace('_', '')}"
        )

        cb.cls(cls_name)
        cb.docstring(
            f"Tests for: {entry.qualified_name}\n"
            f"Priority score : {entry.score:.1f}\n"
            f"Fan-in         : {entry.fan_in} callers\n"
            f"Reachable      : {entry.reachable} downstream scopes"
        )
        cb.blank()

        cb.func("test_basic_call")
        cb.docstring(
            f"Smoke test: {entry.qualified_name} must not raise with minimal valid inputs."
        )
        if entry.is_method:
            cb.todo(f"instantiate {entry.class_name} and call .{short}()")
        else:
            cb.todo(f"arrange inputs for {entry.qualified_name}")
        cb.line(f"# result = {call_str}")
        cb.line("# assert result is not None")
        cb.line("pass  # TODO: fill in")
        cb.end()
        cb.blank()

        cb.end()  # class
        cb.blank()

    return cb.render()


# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------


def _field_sentinel(
    field_info: dict, tf: TestFile | None = None, config: ProjectConfig | None = None
) -> str:
    """Return a sentinel value for a dataclass field, tracking imports."""
    ann = field_info.get("type")
    val = sentinel(ann, config)
    if tf is not None:
        for mod in sentinel_imports(ann):
            tf.add_import(mod)
        if sentinel_needs_cast(ann, config):
            tf.add_typing_import("Any")
            tf.add_typing_import("cast")
    return val


def _sentinel_for_annotation(
    annotation: str | None,
    tf: TestFile | None = None,
    config: ProjectConfig | None = None,
) -> str:
    """Return a sentinel value for a type annotation, tracking imports."""
    val = sentinel(annotation, config)
    if tf is not None:
        for mod in sentinel_imports(annotation):
            tf.add_import(mod)
        if sentinel_needs_cast(annotation, config):
            tf.add_typing_import("Any")
            tf.add_typing_import("cast")
    return val


def _sig_from_params(params: list[Any]) -> str:
    """Build a call-site argument string from a list of ParamInfo-like
    objects (``orchestrata_ast.models.ParamInfo``: ``.name``,
    ``.type_annotation``, ``.kind``)."""
    parts: list[str] = []
    for p in params:
        if p.name in ("self", "cls") or p.kind in ("varargs", "kwargs"):
            continue
        parts.append(f"{p.name}={sentinel(p.type_annotation)}")
    return ", ".join(parts)