import pytest

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


@pytest.mark.blends_test_group("stack_unittesting")
def test_empty_result_for_non_reference_node() -> None:
    views, database = setup_multi_file_database({"cross_file_a.py": 1})
    view = views[1]

    result = resolve_definitions_multi_file(
        view,
        database,
        request=MultiFileResolutionRequest(file_handle=1, ref_node_index=0),
    )

    assert result == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_empty_result_when_file_handle_absent() -> None:
    views, database = setup_multi_file_database({"case_closure.py": 1})
    view = views[1]
    symbol_id = view.symbols.symbol_to_id.get("captured")
    assert symbol_id is not None
    ref_nodes = find_symbol_nodes(view, symbol_id=symbol_id, is_definition=False)
    assert ref_nodes

    result = resolve_definitions_multi_file(
        view,
        database,
        request=MultiFileResolutionRequest(file_handle=99, ref_node_index=ref_nodes[0]),
    )

    assert result == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_stats_files_considered_counts_loaded_files() -> None:
    views, database = setup_multi_file_database({"case_closure.py": 1, "cross_file_a.py": 2})
    view = views[1]
    symbol_id = view.symbols.symbol_to_id.get("captured")
    assert symbol_id is not None
    ref_nodes = find_symbol_nodes(view, symbol_id=symbol_id, is_definition=False)
    assert ref_nodes

    output = resolve_definitions_multi_file(
        view,
        database,
        request=MultiFileResolutionRequest(
            file_handle=1,
            ref_node_index=ref_nodes[0],
            include_stats=True,
            config=ForwardStitcherConfig(
                max_phases=QUERY_MAX_PHASES,
                max_total_work=QUERY_MAX_WORK,
            ),
        ),
    )
    assert isinstance(output, tuple)
    _result, stats = output

    assert stats.files_considered == 1


@pytest.mark.blends_test_group("stack_unittesting")
def test_cancelled_resolution_returns_cancelled_flag() -> None:
    views, database = setup_multi_file_database({"case_closure.py": 1})
    view = views[1]
    symbol_id = view.symbols.symbol_to_id.get("captured")
    assert symbol_id is not None
    ref_nodes = find_symbol_nodes(view, symbol_id=symbol_id, is_definition=False)
    assert ref_nodes

    output = resolve_definitions_multi_file(
        view,
        database,
        request=MultiFileResolutionRequest(
            file_handle=1,
            ref_node_index=ref_nodes[0],
            include_stats=True,
            config=ForwardStitcherConfig(deadline=0.0),
        ),
    )
    assert isinstance(output, tuple)
    result, stats = output

    assert stats.cancelled is True
    assert result == []


@pytest.mark.blends_test_group("stack_unittesting")
def test_reference_resolves_to_source_definition() -> None:
    file_map = {"cross_file_c.py": 1, "cross_file_d.py": 2}
    views, database = setup_multi_file_database(file_map)
    fh_c = file_map["cross_file_c.py"]
    fh_d = file_map["cross_file_d.py"]
    view_d = views[fh_d]
    symbol_id = view_d.symbols.symbol_to_id.get("exported_value")
    assert symbol_id is not None
    ref_nodes = find_symbol_nodes(view_d, symbol_id=symbol_id, is_definition=False)
    assert ref_nodes, "cross_file_d must contain a reference to exported_value"

    result = resolve_definitions_multi_file(
        view_d,
        database,
        request=MultiFileResolutionRequest(
            file_handle=fh_d,
            ref_node_index=ref_nodes[0],
            config=ForwardStitcherConfig(
                max_phases=QUERY_MAX_PHASES,
                max_total_work=QUERY_MAX_WORK,
            ),
        ),
        views=views,
    )

    assert isinstance(result, list)
    assert len(result) >= 1, "exported_value must resolve to at least one definition"
    file_handles = [fh for fh, _ in result]
    assert fh_c in file_handles, (
        f"exported_value source is in cross_file_c (handle={fh_c}), got {file_handles}"
    )


@pytest.mark.blends_test_group("stack_unittesting")
def test_result_tuples_attribute_correct_file_handles() -> None:
    views, database = setup_multi_file_database({"case_closure.py": 1, "cross_file_a.py": 2})
    view = views[1]
    symbol_id = view.symbols.symbol_to_id.get("captured")
    assert symbol_id is not None
    ref_nodes = find_symbol_nodes(view, symbol_id=symbol_id, is_definition=False)
    assert ref_nodes

    result = resolve_definitions_multi_file(
        view,
        database,
        request=MultiFileResolutionRequest(
            file_handle=1,
            ref_node_index=ref_nodes[0],
            config=ForwardStitcherConfig(
                max_phases=QUERY_MAX_PHASES,
                max_total_work=QUERY_MAX_WORK,
            ),
        ),
    )
    assert isinstance(result, list)
    assert len(result) >= 1, "Expected at least one definition of 'captured'"
    valid_handles = set(database.list_files())
    for fh, _idx in result:
        assert fh in valid_handles, f"File handle {fh} not in database {valid_handles}"


@pytest.mark.blends_test_group("stack_unittesting")
def test_javascript_reference_resolves_to_source_definition() -> None:
    file_map = {"cross_file_c.js": 1, "cross_file_d.js": 2}
    views, database = setup_multi_file_database(file_map)
    fh_c = file_map["cross_file_c.js"]
    fh_d = file_map["cross_file_d.js"]
    view_d = views[fh_d]
    symbol_id = view_d.symbols.symbol_to_id.get("exportedValue")
    assert symbol_id is not None
    ref_nodes = find_symbol_nodes(view_d, symbol_id=symbol_id, is_definition=False)
    assert ref_nodes, "cross_file_d.js must contain a reference to exportedValue"

    result = resolve_definitions_multi_file(
        view_d,
        database,
        request=MultiFileResolutionRequest(
            file_handle=fh_d,
            ref_node_index=ref_nodes[0],
            config=ForwardStitcherConfig(
                max_phases=QUERY_MAX_PHASES,
                max_total_work=QUERY_MAX_WORK,
            ),
        ),
        views=views,
    )

    assert isinstance(result, list)
    assert len(result) >= 1, "exportedValue must resolve to at least one definition"
    file_handles = [fh for fh, _ in result]
    assert fh_c in file_handles, (
        f"exportedValue source is in cross_file_c.js (handle={fh_c}), got {file_handles}"
    )


@pytest.mark.blends_test_group("stack_unittesting")
def test_sentinel_lazy_loads_only_direct_dependency() -> None:
    file_map = {"mf_core.py": 1, "mf_transport.py": 2}
    transport_fh = file_map["mf_transport.py"]
    views, database = setup_multi_file_database(file_map)
    view = views[transport_fh]
    symbol_id = view.symbols.symbol_to_id.get("SENTINEL")
    assert symbol_id is not None
    ref_nodes = find_symbol_nodes(view, symbol_id=symbol_id, is_definition=False)
    assert ref_nodes

    output = resolve_definitions_multi_file(
        view,
        database,
        request=MultiFileResolutionRequest(
            file_handle=transport_fh,
            ref_node_index=ref_nodes[0],
            include_stats=True,
            config=ForwardStitcherConfig(
                max_phases=QUERY_MAX_PHASES,
                max_total_work=QUERY_MAX_WORK,
            ),
        ),
        views=views,
    )
    assert isinstance(output, tuple)
    _, stats = output
    assert stats.files_considered == len(file_map)


@pytest.mark.blends_test_group("stack_unittesting")
def test_unrelated_files_in_db_do_not_increase_files_loaded() -> None:
    all_files = {
        "mf_core.py": 1,
        "mf_transport.py": 2,
        "mf_codec.py": 3,
        "mf_processor.py": 4,
        "mf_orchestrator.py": 5,
    }
    direct_deps = {"mf_core.py": 1, "mf_transport.py": 2}
    transport_fh = direct_deps["mf_transport.py"]
    views, database = setup_multi_file_database(all_files)
    view = views[transport_fh]
    symbol_id = view.symbols.symbol_to_id.get("SENTINEL")
    assert symbol_id is not None
    ref_nodes = find_symbol_nodes(view, symbol_id=symbol_id, is_definition=False)
    assert ref_nodes

    output = resolve_definitions_multi_file(
        view,
        database,
        request=MultiFileResolutionRequest(
            file_handle=transport_fh,
            ref_node_index=ref_nodes[0],
            include_stats=True,
            config=ForwardStitcherConfig(
                max_phases=QUERY_MAX_PHASES,
                max_total_work=QUERY_MAX_WORK,
            ),
        ),
        views=views,
    )
    assert isinstance(output, tuple)
    _, stats = output
    assert stats.files_considered == len(direct_deps)


@pytest.mark.blends_test_group("stack_unittesting")
def test_absent_dependency_does_not_load_extra_files() -> None:
    file_map = {"mf_transport.py": 2}
    transport_fh = file_map["mf_transport.py"]
    views, database = setup_multi_file_database(file_map)
    view = views[transport_fh]
    symbol_id = view.symbols.symbol_to_id.get("SENTINEL")
    assert symbol_id is not None
    ref_nodes = find_symbol_nodes(view, symbol_id=symbol_id, is_definition=False)
    assert ref_nodes

    output = resolve_definitions_multi_file(
        view,
        database,
        request=MultiFileResolutionRequest(
            file_handle=transport_fh,
            ref_node_index=ref_nodes[0],
            include_stats=True,
            config=ForwardStitcherConfig(
                max_phases=QUERY_MAX_PHASES,
                max_total_work=QUERY_MAX_WORK,
            ),
        ),
        views=views,
    )
    assert isinstance(output, tuple)
    _, stats = output
    assert stats.files_considered == len(file_map)
