"""Behave steps for UKO Layer 2 detail-level map tests.

Covers DetailLevelMap base maps, DetailLevelMapBuilder,
``build_effective_map``, and unknown-level resolution.

All step definitions use the ``for uko_l2`` suffix to avoid
AmbiguousStep collisions with other feature files.
"""

from __future__ import annotations

import copy
from types import MappingProxyType

from _uko_l2_test_helpers import capture_error
from behave import given, then, when
from behave.runner import Context

from cleveragents.acms.uko.detail_level_maps import (
    CODE_DETAIL_LEVEL_MAP,
    FUNC_DETAIL_LEVEL_MAP,
    OO_DETAIL_LEVEL_MAP,
    PROC_DETAIL_LEVEL_MAP,
    DetailLevelMapBuilder,
    build_effective_map,
)
from cleveragents.domain.models.acms.crp import (
    DetailLevelCycleError,
    DetailLevelMap,
)

# ---------------------------------------------------------------------------
# DetailLevelMap -- base maps
# ---------------------------------------------------------------------------


@given("the uko-code detail level map for uko_l2")
def step_get_code_map(ctx: Context) -> None:
    ctx.detail_map = CODE_DETAIL_LEVEL_MAP


@given("the uko-oo detail level map for uko_l2")
def step_get_oo_map(ctx: Context) -> None:
    ctx.detail_map = OO_DETAIL_LEVEL_MAP


@given("the uko-func detail level map for uko_l2")
def step_get_func_map(ctx: Context) -> None:
    ctx.detail_map = FUNC_DETAIL_LEVEL_MAP


@given("the uko-proc detail level map for uko_l2")
def step_get_proc_map(ctx: Context) -> None:
    ctx.detail_map = PROC_DETAIL_LEVEL_MAP


@then('the map should have domain "{expected}" for uko_l2')
def step_check_map_domain(ctx: Context, expected: str) -> None:
    assert ctx.detail_map.domain == expected


@then("the map should have max_depth {expected:d} for uko_l2")
def step_check_map_max_depth(ctx: Context, expected: int) -> None:
    assert ctx.detail_map.max_depth == expected


@then('the map should resolve "{name}" to {expected:d} for uko_l2')
def step_check_map_resolve_name(ctx: Context, name: str, expected: int) -> None:
    assert ctx.detail_map.resolve(name) == expected


@then("the map should have {count:d} levels for uko_l2")
def step_check_map_level_count(ctx: Context, count: int) -> None:
    assert len(ctx.detail_map.effective_levels()) == count


@then("the map should resolve integer {value:d} to {expected:d} for uko_l2")
def step_check_map_resolve_int(ctx: Context, value: int, expected: int) -> None:
    assert ctx.detail_map.resolve(value) == expected


@then('the map parent domain should be "{expected}" for uko_l2')
def step_check_map_parent_domain(ctx: Context, expected: str) -> None:
    assert ctx.detail_map.parent is not None
    assert ctx.detail_map.parent.domain == expected


# ---------------------------------------------------------------------------
# DetailLevelMapBuilder
# ---------------------------------------------------------------------------


@given(
    'a detail level map builder with parent "{parent}" and domain "{domain}" for uko_l2'
)
def step_create_builder(ctx: Context, parent: str, domain: str) -> None:
    parent_map = CODE_DETAIL_LEVEL_MAP if parent == "uko-code:" else None
    assert parent_map is not None, f"Unknown parent: {parent}"
    ctx.builder = DetailLevelMapBuilder(parent_map, domain)


@when('I insert "{name}" after "{after}" for uko_l2')
def step_builder_insert(ctx: Context, name: str, after: str) -> None:
    ctx.builder.insert_after(after, name)


@when("I build the map for uko_l2")
def step_builder_build(ctx: Context) -> None:
    ctx.built_map = ctx.builder.build()


@then('the built map should have domain "{expected}" for uko_l2')
def step_check_built_domain(ctx: Context, expected: str) -> None:
    assert ctx.built_map.domain == expected


@then('the built map should resolve "{name}" to {expected:d} for uko_l2')
def step_check_built_resolve(ctx: Context, name: str, expected: int) -> None:
    assert ctx.built_map.resolve(name) == expected


@then("the built map should have {count:d} levels for uko_l2")
def step_check_built_levels(ctx: Context, count: int) -> None:
    assert len(ctx.built_map.effective_levels()) == count


@when('I try to insert after non-existent level "{name}" for uko_l2')
def step_insert_nonexistent(ctx: Context, name: str) -> None:
    with capture_error(ctx, ValueError):
        ctx.builder.insert_after(name, "TEST")


@then("a value error should be raised for uko_l2")
def step_check_value_error(ctx: Context) -> None:
    assert ctx.error is not None
    assert isinstance(ctx.error, ValueError)


@when("I try to create a builder with empty domain for uko_l2")
def step_create_builder_empty_domain(ctx: Context) -> None:
    with capture_error(ctx, ValueError):
        DetailLevelMapBuilder(CODE_DETAIL_LEVEL_MAP, "")


@when('I try to insert empty level name after "{after}" for uko_l2')
def step_insert_empty_name(ctx: Context, after: str) -> None:
    with capture_error(ctx, ValueError):
        ctx.builder.insert_after(after, "")


@when('I try to insert "{name}" after empty level name for uko_l2')
def step_insert_after_empty(ctx: Context, name: str) -> None:
    with capture_error(ctx, ValueError):
        ctx.builder.insert_after("", name)


@then('the builder parent domain should be "{expected}" for uko_l2')
def step_check_builder_parent(ctx: Context, expected: str) -> None:
    assert ctx.builder.parent.domain == expected


@then('the builder domain should be "{expected}" for uko_l2')
def step_check_builder_domain(ctx: Context, expected: str) -> None:
    assert ctx.builder.domain == expected


# ---------------------------------------------------------------------------
# build_effective_map function
# ---------------------------------------------------------------------------


@when("I build an effective map with no insertions for uko_l2")
def step_build_effective_no_insert(ctx: Context) -> None:
    ctx.effective = build_effective_map(ctx.detail_map, [])


@when('I build an effective map with insertion "{name}" after "{after}" for uko_l2')
def step_build_effective_insert(ctx: Context, name: str, after: str) -> None:
    ctx.effective = build_effective_map(ctx.detail_map, [(after, name)])


@when('I try to build an effective map with insertion after "{after}" for uko_l2')
def step_build_effective_invalid(ctx: Context, after: str) -> None:
    with capture_error(ctx, ValueError):
        build_effective_map(ctx.detail_map, [(after, "TEST")])


@then("the effective map should have {count:d} entries for uko_l2")
def step_check_effective_count(ctx: Context, count: int) -> None:
    assert len(ctx.effective) == count


@then(
    'the effective map entry {index:d} should be "{name}" at depth {depth:d} for uko_l2'
)
def step_check_effective_entry(ctx: Context, index: int, name: str, depth: int) -> None:
    entry_name, entry_depth = ctx.effective[index]
    assert entry_name == name, f"Expected '{name}' but got '{entry_name}'"
    assert entry_depth == depth, f"Expected depth {depth} but got {entry_depth}"


# ---------------------------------------------------------------------------
# Unknown level resolution
# ---------------------------------------------------------------------------


@when('I try to resolve unknown level "{name}" for uko_l2')
def step_resolve_unknown(ctx: Context, name: str) -> None:
    with capture_error(ctx, ValueError):
        ctx.detail_map.resolve(name)


# ---------------------------------------------------------------------------
# Fresh DetailLevelMap (register / mutate)
# ---------------------------------------------------------------------------


@given("a fresh detail level map for uko_l2")
def step_create_fresh_map(ctx: Context) -> None:
    ctx.fresh_map = DetailLevelMap(
        domain="test:", parent=None, levels={"BASE": 0}, max_depth=10
    )


@when('I register level "{name}" with value {value:d} for uko_l2')
def step_register_level(ctx: Context, name: str, value: int) -> None:
    ctx.fresh_map.register(name, value)


@then('the fresh map should resolve "{name}" to {expected:d} for uko_l2')
def step_check_fresh_resolve(ctx: Context, name: str, expected: int) -> None:
    assert ctx.fresh_map.resolve(name) == expected


@when('I try to register level "{name}" with value {value:d} for uko_l2')
def step_try_register_negative(ctx: Context, name: str, value: int) -> None:
    with capture_error(ctx, ValueError):
        ctx.fresh_map.register(name, value)


@when("I try to mutate the levels dict for uko_l2")
def step_mutate_levels(ctx: Context) -> None:
    with capture_error(ctx, TypeError):
        ctx.detail_map.levels["HACK"] = 99


@then("a type error should be raised for uko_l2")
def step_check_type_error(ctx: Context) -> None:
    assert ctx.error is not None
    assert isinstance(ctx.error, TypeError)


@when('I try to insert duplicate level "{name}" after "{after}" for uko_l2')
def step_try_insert_duplicate(ctx: Context, name: str, after: str) -> None:
    with capture_error(ctx, ValueError):
        ctx.builder.insert_after(after, name)


@when("I try to build an effective map with duplicate level for uko_l2")
def step_build_effective_duplicate(ctx: Context) -> None:
    with capture_error(ctx, ValueError):
        build_effective_map(ctx.detail_map, [("MEMBER_LISTING", "MODULE_LISTING")])


@when("I try to mutate effective_levels for uko_l2")
def step_mutate_effective_levels(ctx: Context) -> None:
    with capture_error(ctx, TypeError):
        effective = ctx.detail_map.effective_levels()
        effective["HACK"] = 99  # type: ignore[index]  # deliberate mutation test


# ---------------------------------------------------------------------------
# Review findings: H3 — MappingProxyType serialization
# ---------------------------------------------------------------------------


@when("I serialize the detail level map to JSON for uko_l2")
def step_serialize_map_json(ctx: Context) -> None:
    ctx.serialized_json = ctx.detail_map.model_dump_json()


@then('the serialized JSON should contain "{expected}" for uko_l2')
def step_check_serialized_json(ctx: Context, expected: str) -> None:
    assert expected in ctx.serialized_json


@when("I dump the detail level map to dict for uko_l2")
def step_dump_map_dict(ctx: Context) -> None:
    ctx.dumped_dict = ctx.detail_map.model_dump()


@then('the dumped dict levels should contain "{expected}" for uko_l2')
def step_check_dumped_dict(ctx: Context, expected: str) -> None:
    assert expected in ctx.dumped_dict["levels"]


# ---------------------------------------------------------------------------
# Review findings: M5 — cycle guard in resolve()
# ---------------------------------------------------------------------------


@given("a detail level map with circular parent for uko_l2")
def step_create_circular_map(ctx: Context) -> None:
    # Build two maps that point to each other
    map_a = DetailLevelMap(domain="cycle-a:", parent=None, levels={"A": 0}, max_depth=5)
    map_b = DetailLevelMap(
        domain="cycle-b:", parent=map_a, levels={"B": 1}, max_depth=5
    )
    # Introduce cycle: map_a.parent -> map_b
    object.__setattr__(map_a, "parent", map_b)
    ctx.circular_map = map_a


@when('I try to resolve level "{name}" on the circular map for uko_l2')
def step_resolve_circular(ctx: Context, name: str) -> None:
    with capture_error(ctx, DetailLevelCycleError):
        ctx.circular_map.resolve(name)


@then("a cycle error should be raised for uko_l2")
def step_check_cycle_error(ctx: Context) -> None:
    assert ctx.error is not None
    assert isinstance(ctx.error, DetailLevelCycleError)


# ---------------------------------------------------------------------------
# Review findings: H2 — levels re-frozen on assignment
# ---------------------------------------------------------------------------


@when("I assign a dict to the levels field for uko_l2")
def step_assign_dict_levels(ctx: Context) -> None:
    ctx.fresh_map.levels = {"NEW_LEVEL": 3}  # type: ignore[assignment]  # deliberate test
    ctx.levels_type = type(ctx.fresh_map.levels)
    ctx.is_mapping_proxy = isinstance(ctx.fresh_map.levels, MappingProxyType)


@then("the levels should still be an immutable MappingProxy for uko_l2")
def step_check_levels_refrozen(ctx: Context) -> None:
    assert ctx.is_mapping_proxy, f"Expected MappingProxyType but got {ctx.levels_type}"
    # Also verify it's actually immutable.
    # Note: capture_error() is not used here because the pattern is
    # "assert that an exception IS raised" (inline assertion), not
    # "capture the error for a later @then step".
    try:
        ctx.fresh_map.levels["HACK"] = 99  # type: ignore[index]  # deliberate mutation test
        msg = "Expected TypeError on mutation"
        raise AssertionError(msg)
    except TypeError:
        pass


# ---------------------------------------------------------------------------
# Review findings: F7 — cycle guard in effective_levels()
# ---------------------------------------------------------------------------


@when("I try to get effective levels on the circular map for uko_l2")
def step_effective_levels_circular(ctx: Context) -> None:
    with capture_error(ctx, DetailLevelCycleError):
        ctx.circular_map.effective_levels()


# ---------------------------------------------------------------------------
# Review findings: F13 — deepcopy support
# ---------------------------------------------------------------------------


@when("I deep copy the detail level map for uko_l2")
def step_deep_copy_map(ctx: Context) -> None:
    ctx.copied_map = copy.deepcopy(ctx.detail_map)


@then('the copied map should have domain "{expected}" for uko_l2')
def step_check_copied_domain(ctx: Context, expected: str) -> None:
    assert ctx.copied_map.domain == expected


@then('the copied map should resolve "{name}" to {expected:d} for uko_l2')
def step_check_copied_resolve(ctx: Context, name: str, expected: int) -> None:
    assert ctx.copied_map.resolve(name) == expected


@then("the copied map should not be the same object for uko_l2")
def step_check_copied_identity(ctx: Context) -> None:
    assert ctx.copied_map is not ctx.detail_map
