"""Step definitions for tui_reference_parser_coverage.feature.

These steps target specific uncovered lines in reference_parser.py:
- Lines 59-60:  ValueError fallback in _catalog file listing
- Lines 113-114: Escaped \\@ token pass-through
- Lines 123-126: Category parsing from @category:query syntax
- Lines 129-135: Typed reference resolves successfully
- Lines 139-141: Typed reference fails to resolve (unresolved)
- Lines 160-161: Untyped reference fails across all categories
"""

from pathlib import Path
from unittest.mock import patch

from behave import given, then, when

from cleveragents.tui.input import reference_parser as rp_module
from cleveragents.tui.input.reference_parser import (
    _REFERENCE_TYPES,
    _catalog,
    _catalog_cache,
    parse_references,
)


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the reference parser module is imported")
def step_reference_parser_imported(context):
    """Verify the module is importable."""
    assert parse_references is not None
    assert _catalog is not None


# ---------------------------------------------------------------------------
# Helpers - mock catalog setup
# ---------------------------------------------------------------------------
def _empty_catalog():
    """Return a catalog with empty lists for every category."""
    return {cat: [] for cat in _REFERENCE_TYPES}


def _make_catalog(**overrides):
    """Return an empty catalog with specific category overrides applied."""
    catalog = _empty_catalog()
    catalog.update(overrides)
    return catalog


# ---------------------------------------------------------------------------
# Given steps - mock catalog variants
# ---------------------------------------------------------------------------
@given("the catalog is mocked with minimal data")
def step_catalog_mocked_minimal(context):
    """Prepare a minimal catalog mock that returns no entries."""
    context.catalog_patch = patch.object(
        rp_module, "_catalog", return_value=_empty_catalog()
    )
    context.catalog_patch.start()
    context.add_cleanup(context.catalog_patch.stop)


@given('the catalog is mocked with resource entries "{entry1}" and "{entry2}"')
def step_catalog_mocked_resources(context, entry1, entry2):
    """Mock the catalog to contain specific resource entries."""
    catalog = _make_catalog(resource=sorted([entry1, entry2]))
    context.catalog_patch = patch.object(rp_module, "_catalog", return_value=catalog)
    context.catalog_patch.start()
    context.add_cleanup(context.catalog_patch.stop)


@given('the catalog is mocked with plan entries "{entry1}" and "{entry2}"')
def step_catalog_mocked_plans(context, entry1, entry2):
    """Mock the catalog to contain specific plan entries."""
    catalog = _make_catalog(plan=sorted([entry1, entry2]))
    context.catalog_patch = patch.object(rp_module, "_catalog", return_value=catalog)
    context.catalog_patch.start()
    context.add_cleanup(context.catalog_patch.stop)


@given('the catalog is mocked with actor entries "{entry1}" and "{entry2}"')
def step_catalog_mocked_actors(context, entry1, entry2):
    """Mock the catalog to contain specific actor entries."""
    catalog = _make_catalog(actor=sorted([entry1, entry2]))
    context.catalog_patch = patch.object(rp_module, "_catalog", return_value=catalog)
    context.catalog_patch.start()
    context.add_cleanup(context.catalog_patch.stop)


@given("the catalog is mocked with empty lists for all categories")
def step_catalog_mocked_empty(context):
    """Mock the catalog to return empty lists for every category."""
    context.catalog_patch = patch.object(
        rp_module, "_catalog", return_value=_empty_catalog()
    )
    context.catalog_patch.start()
    context.add_cleanup(context.catalog_patch.stop)


# ---------------------------------------------------------------------------
# Given steps - catalog cache / os.walk mocking for lines 59-60
# ---------------------------------------------------------------------------
@given("the catalog cache is invalidated")
def step_invalidate_catalog_cache(context):
    """Reset the module-level catalog cache so _catalog() rebuilds."""
    context.original_cache = {
        "cwd": _catalog_cache.get("cwd"),
        "created_at": _catalog_cache.get("created_at"),
        "catalog": _catalog_cache.get("catalog"),
    }
    _catalog_cache["cwd"] = None
    _catalog_cache["created_at"] = 0.0
    _catalog_cache["catalog"] = None

    def restore_cache():
        _catalog_cache["cwd"] = context.original_cache["cwd"]
        _catalog_cache["created_at"] = context.original_cache["created_at"]
        _catalog_cache["catalog"] = context.original_cache["catalog"]

    context.add_cleanup(restore_cache)


@given("os.walk is mocked to return a path outside the working directory")
def step_mock_os_walk_outside_cwd(context):
    """Mock os.walk to yield a directory outside cwd.

    When Path.relative_to(cwd) is called on a file from an outside dir,
    it raises ValueError, triggering the fallback on lines 59-60.
    """
    outside_root = "/outside/foreign/dir"
    mock_filenames = ["external_file.txt"]

    def fake_walk(top, followlinks=False):
        # First yield the real cwd with nothing, then yield the outside path
        yield outside_root, [], mock_filenames

    context.walk_patch = patch.object(rp_module.os, "walk", side_effect=fake_walk)
    context.walk_patch.start()

    # Also mock the actors/tools/skills dirs so they don't touch the real FS
    context.is_dir_patch = patch.object(Path, "is_dir", return_value=False)
    context.is_dir_patch.start()

    def cleanup():
        context.walk_patch.stop()
        context.is_dir_patch.stop()

    context.add_cleanup(cleanup)


# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when('I parse references from the line "{line}"')
def step_parse_references(context, line):
    """Call parse_references with the given line."""
    context.parse_result = parse_references(line)


@when("I build the catalog")
def step_build_catalog(context):
    """Call _catalog() directly to exercise the file-listing code."""
    context.catalog_result = _catalog()


# ---------------------------------------------------------------------------
# Then steps - expanded line
# ---------------------------------------------------------------------------
@then('the expanded line should be "{expected}"')
def step_expanded_line_equals(context, expected):
    """Verify the expanded line matches exactly."""
    actual = context.parse_result.expanded_line
    assert actual == expected, f"Expected '{expected}', got '{actual}'"


@then('the expanded line should contain "{fragment}"')
def step_expanded_line_contains(context, fragment):
    """Verify the expanded line contains a fragment."""
    actual = context.parse_result.expanded_line
    assert fragment in actual, f"Expected '{fragment}' in '{actual}'"


# ---------------------------------------------------------------------------
# Then steps - matches
# ---------------------------------------------------------------------------
@then("there should be no resolved matches")
def step_no_matches(context):
    """Verify no references were resolved."""
    assert len(context.parse_result.matches) == 0, (
        f"Expected 0 matches, got {context.parse_result.matches}"
    )


@then("there should be {count:d} resolved match")
def step_match_count(context, count):
    """Verify the exact number of resolved matches."""
    actual = len(context.parse_result.matches)
    assert actual == count, (
        f"Expected {count} match(es), got {actual}: {context.parse_result.matches}"
    )


@then('the resolved match category should be "{category}"')
def step_match_category(context, category):
    """Verify the first resolved match has the expected category."""
    match = context.parse_result.matches[0]
    assert match.category == category, (
        f"Expected category '{category}', got '{match.category}'"
    )


# ---------------------------------------------------------------------------
# Then steps - unresolved tokens
# ---------------------------------------------------------------------------
@then("there should be no unresolved tokens")
def step_no_unresolved(context):
    """Verify no tokens were left unresolved."""
    assert len(context.parse_result.unresolved_tokens) == 0, (
        f"Expected 0 unresolved, got {context.parse_result.unresolved_tokens}"
    )


@then('"{token}" should be in the unresolved tokens')
def step_token_unresolved(context, token):
    """Verify a specific token is listed as unresolved."""
    assert token in context.parse_result.unresolved_tokens, (
        f"Expected '{token}' in unresolved list "
        f"{context.parse_result.unresolved_tokens}"
    )


# ---------------------------------------------------------------------------
# Then steps - catalog ValueError fallback (lines 59-60)
# ---------------------------------------------------------------------------
@then("the catalog resource list should contain the absolute fallback path")
def step_catalog_has_absolute_path(context):
    """Verify that the catalog collected the file using its absolute path.

    When relative_to(cwd) raises ValueError, the code falls back to
    ``(root_path / filename).as_posix()`` -- the absolute path.
    """
    resources = context.catalog_result.get("resource", [])
    expected = "/outside/foreign/dir/external_file.txt"
    assert expected in resources, (
        f"Expected '{expected}' in resource list, got {resources}"
    )
