import pytest

from blends.stack import (
    MultiFileResolutionRequest,
    resolve_definitions_multi_file,
)
from blends.stack.forward_stitcher import ForwardStitcherConfig
from blends.stack.partial_path_database import PartialPathDatabase
from test.stack.test_helpers import (
    QUERY_MAX_PHASES,
    QUERY_MAX_WORK,
    find_symbol_nodes,
    setup_multi_file_database,
)

_MF_CORE = "mf_core.py"
_MF_TRANSPORT = "mf_transport.py"
_MF_PROCESSOR = "mf_processor.py"
_FH_CORE = 1
_FH_TRANSPORT = 2
_FH_PROCESSOR = 4

_CONFIG = ForwardStitcherConfig(
    max_phases=QUERY_MAX_PHASES,
    max_total_work=QUERY_MAX_WORK,
)


def _resolve(symbol: str, *, from_fh: int, views: dict, database: PartialPathDatabase) -> list:  # type: ignore[type-arg]
    view = views[from_fh]
    symbol_id = view.symbols.symbol_to_id.get(symbol)
    assert symbol_id is not None, f"Symbol '{symbol}' not found in view for fh={from_fh}"
    ref_nodes = find_symbol_nodes(view, symbol_id=symbol_id, is_definition=False)
    assert ref_nodes, f"No reference nodes for '{symbol}' in fh={from_fh}"
    result = resolve_definitions_multi_file(
        view,
        database,
        request=MultiFileResolutionRequest(
            file_handle=from_fh,
            ref_node_index=ref_nodes[0],
            config=_CONFIG,
        ),
        views=views,
    )
    assert isinstance(result, list)
    return result


def _assert_resolves_to(
    symbol: str,
    *,
    from_fh: int,
    expected_fh: int,
    views: dict,  # type: ignore[type-arg]
    database: PartialPathDatabase,
) -> None:
    result = _resolve(symbol, from_fh=from_fh, views=views, database=database)
    file_handles = [fh for fh, _ in result]
    assert expected_fh in file_handles, (
        f"'{symbol}' from fh={from_fh} should resolve to fh={expected_fh}, got {file_handles}"
    )


@pytest.mark.blends_test_group("stack_unittesting")
def test_constant_imports_resolve_to_source() -> None:
    views, database = setup_multi_file_database({_MF_CORE: _FH_CORE, _MF_TRANSPORT: _FH_TRANSPORT})

    for symbol in ("SENTINEL", "MAX_POOL_SIZE", "HTTP_UNAVAILABLE"):
        _assert_resolves_to(
            symbol, from_fh=_FH_TRANSPORT, expected_fh=_FH_CORE, views=views, database=database
        )


@pytest.mark.blends_test_group("stack_unittesting")
def test_class_imports_resolve_to_source() -> None:
    views, database = setup_multi_file_database({_MF_CORE: _FH_CORE, _MF_TRANSPORT: _FH_TRANSPORT})

    for symbol in ("CoreError", "RetryPolicy", "ConnectionConfig"):
        _assert_resolves_to(
            symbol, from_fh=_FH_TRANSPORT, expected_fh=_FH_CORE, views=views, database=database
        )


@pytest.mark.blends_test_group("stack_unittesting")
def test_single_hop_to_defining_file() -> None:
    views, database = setup_multi_file_database(
        {_MF_TRANSPORT: _FH_TRANSPORT, _MF_PROCESSOR: _FH_PROCESSOR}
    )

    for symbol in ("TransportError", "ConnectionPool", "Request"):
        _assert_resolves_to(
            symbol, from_fh=_FH_PROCESSOR, expected_fh=_FH_TRANSPORT, views=views, database=database
        )


@pytest.mark.blends_test_group("stack_unittesting")
def test_two_hop_chain_resolves_to_origin() -> None:
    views, database = setup_multi_file_database(
        {_MF_CORE: _FH_CORE, _MF_TRANSPORT: _FH_TRANSPORT, _MF_PROCESSOR: _FH_PROCESSOR}
    )

    _assert_resolves_to(
        "SENTINEL", from_fh=_FH_PROCESSOR, expected_fh=_FH_CORE, views=views, database=database
    )


@pytest.mark.blends_test_group("stack_unittesting")
def test_direct_import_skips_intermediate() -> None:
    views, database = setup_multi_file_database(
        {_MF_CORE: _FH_CORE, _MF_TRANSPORT: _FH_TRANSPORT, _MF_PROCESSOR: _FH_PROCESSOR}
    )

    for symbol in ("RetryPolicy", "MAX_RETRIES"):
        _assert_resolves_to(
            symbol, from_fh=_FH_PROCESSOR, expected_fh=_FH_CORE, views=views, database=database
        )


_MF_BARE_MODULE = "mf_bare_module.py"
_FH_BARE_MODULE = 10

_MF_ORCHESTRATOR = "mf_orchestrator.py"
_FH_ORCHESTRATOR = 20

_MF_IMPORT_AS_CONSUMER = "mf_import_as_consumer.py"
_FH_IMPORT_AS_CONSUMER = 12


@pytest.mark.blends_test_group("stack_unittesting")
def test_aliased_import_resolves_to_source() -> None:
    views, database = setup_multi_file_database(
        {_MF_CORE: _FH_CORE, _MF_ORCHESTRATOR: _FH_ORCHESTRATOR}
    )
    _assert_resolves_to(
        "BUFFER_SIZE",
        from_fh=_FH_ORCHESTRATOR,
        expected_fh=_FH_CORE,
        views=views,
        database=database,
    )


@pytest.mark.blends_test_group("stack_unittesting")
def test_bare_import_with_alias_is_import_only() -> None:
    views, database = setup_multi_file_database(
        {_MF_BARE_MODULE: _FH_BARE_MODULE, _MF_IMPORT_AS_CONSUMER: _FH_IMPORT_AS_CONSUMER}
    )
    result = _resolve("bm", from_fh=_FH_IMPORT_AS_CONSUMER, views=views, database=database)
    file_handles = [fh for fh, _ in result]
    assert _FH_IMPORT_AS_CONSUMER in file_handles, (
        f"import binding for 'bm' should appear in result, got {file_handles}"
    )
    assert _FH_BARE_MODULE not in file_handles, (
        f"mf_bare_module.py (fh={_FH_BARE_MODULE}) should not be reachable via "
        f"'import X as Z' without member-access resolution, got {file_handles}"
    )


@pytest.mark.blends_test_group("stack_unittesting")
def test_assignment_reexport_resolves_to_source() -> None:
    views, database = setup_multi_file_database(
        {_MF_PROCESSOR: _FH_PROCESSOR, _MF_ORCHESTRATOR: _FH_ORCHESTRATOR}
    )
    _assert_resolves_to(
        "OrchestratorError",
        from_fh=_FH_ORCHESTRATOR,
        expected_fh=_FH_PROCESSOR,
        views=views,
        database=database,
    )


@pytest.mark.blends_test_group("stack_unittesting")
def test_absent_source_returns_import_binding() -> None:
    views, database = setup_multi_file_database({_MF_TRANSPORT: _FH_TRANSPORT})
    result = _resolve("SENTINEL", from_fh=_FH_TRANSPORT, views=views, database=database)

    file_handles = [fh for fh, _ in result]
    assert _FH_TRANSPORT in file_handles, (
        f"Import binding in mf_transport.py (fh={_FH_TRANSPORT}) should be returned as fallback, "
        f"got {file_handles}"
    )
    assert _FH_CORE not in file_handles, (
        f"mf_core.py (fh={_FH_CORE}) is not indexed, should not appear in result, "
        f"got {file_handles}"
    )
