"""Step definitions for team collaboration feature tests.

Covers team roles, permissions, user sessions, session registry,
version stamps, conflict resolution, and the TeamCollaborationService.

Based on issue #863 — feat(server): team collaboration features.
"""

from __future__ import annotations

import re
from time import sleep

from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError

from cleveragents.application.services.team_collab_service import (
    TeamCollaborationService,
)
from cleveragents.domain.models.core.team_collab import (
    TEAM_ROLE_PERMISSIONS,
    ConflictDetectionResult,
    ConflictResolutionStrategy,
    SessionRegistry,
    TeamCollaborationError,
    TeamMember,
    TeamMemberExistsError,
    TeamMemberNotFoundError,
    TeamPermission,
    TeamRole,
    UserSession,
    VersionConflictError,
    VersionStamp,
)

ULID_RE = re.compile(r"^[0-9A-HJKMNP-TV-Z]{26}$")


# ── TeamRole enum ──────────────────────────────────────────────────


@given("the TeamRole enum")
def step_given_team_role_enum(context: Context) -> None:
    context.enum_cls = TeamRole


@then('the TeamRole enum should contain "{value}"')
def step_then_team_role_contains(context: Context, value: str) -> None:
    members = [m.value for m in TeamRole]
    assert value in members, f"{value} not in TeamRole: {members}"


# ── TeamPermission enum ────────────────────────────────────────────


@given("the TeamPermission enum")
def step_given_team_permission_enum(context: Context) -> None:
    context.enum_cls = TeamPermission


@then('the TeamPermission enum should contain "{value}"')
def step_then_team_permission_contains(context: Context, value: str) -> None:
    members = [m.value for m in TeamPermission]
    assert value in members, f"{value} not in TeamPermission: {members}"


# ── ConflictResolutionStrategy enum ────────────────────────────────


@given("the ConflictResolutionStrategy enum")
def step_given_conflict_strategy_enum(context: Context) -> None:
    context.enum_cls = ConflictResolutionStrategy


@then('the ConflictResolutionStrategy enum should contain "{value}"')
def step_then_conflict_strategy_contains(context: Context, value: str) -> None:
    members = [m.value for m in ConflictResolutionStrategy]
    assert value in members, f"{value} not in ConflictResolutionStrategy: {members}"


# ── TEAM_ROLE_PERMISSIONS constant ─────────────────────────────────


@given("the TEAM_ROLE_PERMISSIONS constant")
def step_given_team_role_permissions(context: Context) -> None:
    context.role_permissions = TEAM_ROLE_PERMISSIONS


@then('the "{role}" team role should have {count:d} allowed permissions')
def step_then_team_role_has_n_permissions(
    context: Context, role: str, count: int
) -> None:
    team_role = TeamRole(role)
    perms = TEAM_ROLE_PERMISSIONS[team_role]
    assert len(perms) == count, f"{role} has {len(perms)} permissions, expected {count}"


# ── TeamMember model ───────────────────────────────────────────────


@given('a team member "{user_id}" with role "{role}" and email "{email}"')
def step_given_team_member(
    context: Context, user_id: str, role: str, email: str
) -> None:
    context.team_member = TeamMember(
        user_id=user_id,
        display_name=user_id.title(),
        email=email,
        role=TeamRole(role),
    )


@then('the team member user_id should be "{expected}"')
def step_then_member_user_id(context: Context, expected: str) -> None:
    assert context.team_member.user_id == expected


@then('the team member role should be "{expected}"')
def step_then_member_role(context: Context, expected: str) -> None:
    assert context.team_member.role == TeamRole(expected)


@then('the team member email should be "{expected}"')
def step_then_member_email(context: Context, expected: str) -> None:
    assert context.team_member.email == expected


@when('I try to create a team member with email "{email}"')
def step_when_create_member_bad_email(context: Context, email: str) -> None:
    context.validation_error = None
    try:
        TeamMember(
            user_id="test",
            display_name="Test",
            email=email,
            role=TeamRole.MEMBER,
        )
    except ValidationError as exc:
        context.validation_error = exc


@then("a team member validation error should be raised")
def step_then_team_member_validation_error_raised(context: Context) -> None:
    assert context.validation_error is not None, "Expected ValidationError"


@then('the team member should have "{permission}" permission')
def step_then_member_has_permission(context: Context, permission: str) -> None:
    perm = TeamPermission(permission)
    assert context.team_member.has_permission(perm), (
        f"Expected member to have {permission}"
    )


@then('the team member should not have "{permission}" permission')
def step_then_member_no_permission(context: Context, permission: str) -> None:
    perm = TeamPermission(permission)
    assert not context.team_member.has_permission(perm), (
        f"Expected member NOT to have {permission}"
    )


# ── UserSession model ──────────────────────────────────────────────


@given('a user session for user "{user_id}" on project "{project_id}"')
def step_given_user_session(context: Context, user_id: str, project_id: str) -> None:
    context.user_session = UserSession(
        session_id=str(SessionRegistry().generate_session_id()),
        user_id=user_id,
        display_name=user_id.title(),
        project_id=project_id,
    )


@then('the user session user_id should be "{expected}"')
def step_then_session_user_id(context: Context, expected: str) -> None:
    assert context.user_session.user_id == expected


@then('the user session project_id should be "{expected}"')
def step_then_session_project_id(context: Context, expected: str) -> None:
    assert context.user_session.project_id == expected


@then("the user session should be active")
def step_then_session_active(context: Context) -> None:
    assert context.user_session.is_active is True


@when("the session is deactivated")
def step_when_session_deactivated(context: Context) -> None:
    context.user_session.deactivate()


@then("the user session should not be active")
def step_then_session_not_active(context: Context) -> None:
    assert context.user_session.is_active is False


@when("the session is touched")
def step_when_session_touched(context: Context) -> None:
    context.old_last_active = context.user_session.last_active
    sleep(0.01)
    context.user_session.touch()


@then("the last_active timestamp should be updated")
def step_then_last_active_updated(context: Context) -> None:
    assert context.user_session.last_active >= context.old_last_active


# ── VersionStamp model ─────────────────────────────────────────────


@given('a version stamp for resource "{resource_id}" by user "{user_id}"')
def step_given_version_stamp(context: Context, resource_id: str, user_id: str) -> None:
    context.version_stamp = VersionStamp(
        resource_id=resource_id,
        updated_by=user_id,
    )


@then("the version stamp version should be {expected:d}")
def step_then_stamp_version(context: Context, expected: int) -> None:
    assert context.version_stamp.version == expected


@then('the version stamp updated_by should be "{expected}"')
def step_then_stamp_updated_by(context: Context, expected: str) -> None:
    assert context.version_stamp.updated_by == expected


@when('the version stamp is incremented by "{user_id}"')
def step_when_stamp_incremented(context: Context, user_id: str) -> None:
    context.new_version_stamp = context.version_stamp.increment(user_id)


@then("the new version stamp version should be {expected:d}")
def step_then_new_stamp_version(context: Context, expected: int) -> None:
    assert context.new_version_stamp.version == expected


@then('the new version stamp updated_by should be "{expected}"')
def step_then_new_stamp_updated_by(context: Context, expected: str) -> None:
    assert context.new_version_stamp.updated_by == expected


@then("incrementing with empty user should raise ValueError")
def step_then_increment_empty_raises(context: Context) -> None:
    raised = False
    try:
        context.version_stamp.increment("")
    except ValueError:
        raised = True
    assert raised, "Expected ValueError"


# ── ConflictDetectionResult ────────────────────────────────────────


@given('a conflict detection result with no conflict for "{resource_id}"')
def step_given_no_conflict_result(context: Context, resource_id: str) -> None:
    context.conflict_result = ConflictDetectionResult(
        detected=False,
        resource_id=resource_id,
        resolved=True,
        details="No conflict",
    )


@then("the result detected should be False")
def step_then_result_not_detected(context: Context) -> None:
    assert context.conflict_result.detected is False


@then('the result resource_id should be "{expected}"')
def step_then_result_resource_id(context: Context, expected: str) -> None:
    assert context.conflict_result.resource_id == expected


# ── VersionConflictError ───────────────────────────────────────────


@when(
    'a VersionConflictError is raised for "{resource_id}" '
    "expected {expected:d} actual {actual:d}"
)
def step_when_version_conflict_error(
    context: Context, resource_id: str, expected: int, actual: int
) -> None:
    context.conflict_error = VersionConflictError(
        resource_id=resource_id,
        expected_version=expected,
        actual_version=actual,
    )


@then('the error resource_id should be "{expected}"')
def step_then_error_resource_id(context: Context, expected: str) -> None:
    assert context.conflict_error.resource_id == expected


@then("the error expected_version should be {expected:d}")
def step_then_error_expected_version(context: Context, expected: int) -> None:
    assert context.conflict_error.expected_version == expected


@then("the error actual_version should be {expected:d}")
def step_then_error_actual_version(context: Context, expected: int) -> None:
    assert context.conflict_error.actual_version == expected


@then("creating VersionConflictError with empty resource_id raises ValueError")
def step_then_conflict_error_empty_raises(context: Context) -> None:
    raised = False
    try:
        VersionConflictError(resource_id="", expected_version=1, actual_version=2)
    except ValueError:
        raised = True
    assert raised, "Expected ValueError"


# ── SessionRegistry ────────────────────────────────────────────────


@given("a session registry")
def step_given_session_registry(context: Context) -> None:
    context.session_registry = SessionRegistry()
    context.sessions = {}


@given('a user session "{session_id}" for user "{user_id}" on project "{project_id}"')
def step_given_named_session(
    context: Context, session_id: str, user_id: str, project_id: str
) -> None:
    session = UserSession(
        session_id=session_id,
        user_id=user_id,
        display_name=user_id.title(),
        project_id=project_id,
    )
    if not hasattr(context, "sessions") or context.sessions is None:
        context.sessions = {}
    context.sessions[session_id] = session


@when("the session is registered")
def step_when_session_registered(context: Context) -> None:
    for session in context.sessions.values():
        context.session_registry.register(session)


@when("all sessions are registered")
def step_when_all_sessions_registered(context: Context) -> None:
    for session in context.sessions.values():
        context.session_registry.register(session)


@then('the registry should contain the session "{session_id}"')
def step_then_registry_contains(context: Context, session_id: str) -> None:
    result = context.session_registry.get(session_id)
    assert result is not None, f"Session '{session_id}' not found"


@then('the registry should not contain the session "{session_id}"')
def step_then_registry_not_contains(context: Context, session_id: str) -> None:
    result = context.session_registry.get(session_id)
    assert result is None, f"Session '{session_id}' should not be found"


@then("the registry active count should be {expected:d}")
def step_then_registry_active_count(context: Context, expected: int) -> None:
    assert context.session_registry.active_count == expected, (
        f"Expected {expected}, got {context.session_registry.active_count}"
    )


@when('the session "{session_id}" is unregistered')
def step_when_session_unregistered(context: Context, session_id: str) -> None:
    context.session_registry.unregister(session_id)


@then('the registry should have {count:d} sessions for user "{user_id}"')
def step_then_registry_user_sessions(
    context: Context, count: int, user_id: str
) -> None:
    sessions = context.session_registry.get_sessions_for_user(user_id)
    assert len(sessions) == count, (
        f"Expected {count} sessions for user '{user_id}', got {len(sessions)}"
    )


@then('the registry should have {count:d} sessions for project "{project_id}"')
def step_then_registry_project_sessions(
    context: Context, count: int, project_id: str
) -> None:
    sessions = context.session_registry.get_sessions_for_project(project_id)
    assert len(sessions) == count, (
        f"Expected {count} sessions for project '{project_id}', got {len(sessions)}"
    )


@when("the registry is cleared")
def step_when_registry_cleared(context: Context) -> None:
    context.session_registry.clear()


@then("registering the same session again raises ValueError")
def step_then_duplicate_registration_raises(context: Context) -> None:
    session = next(iter(context.sessions.values()))
    raised = False
    try:
        context.session_registry.register(session)
    except ValueError:
        raised = True
    assert raised, "Expected ValueError for duplicate registration"


@then("the generated session ID should be a valid ULID")
def step_then_generated_id_valid(context: Context) -> None:
    sid = context.session_registry.generate_session_id()
    assert ULID_RE.match(sid), f"'{sid}' is not a valid ULID"


# ── TeamCollaborationService — member management ───────────────────


@given("a team collaboration service")
def step_given_collab_service(context: Context) -> None:
    context.collab_service = TeamCollaborationService()
    context.last_session = None
    context.last_conflict_result = None


@when('I add member "{user_id}" with role "{role}" and email "{email}"')
def step_when_add_member(context: Context, user_id: str, role: str, email: str) -> None:
    context.collab_service.add_member(
        user_id=user_id,
        display_name=user_id.title(),
        email=email,
        role=TeamRole(role),
    )


@then("the service should have {count:d} members")
def step_then_service_member_count(context: Context, count: int) -> None:
    members = context.collab_service.list_members()
    assert len(members) == count, f"Expected {count} members, got {len(members)}"


@then('getting member "{user_id}" should return the member')
def step_then_get_member(context: Context, user_id: str) -> None:
    member = context.collab_service.get_member(user_id)
    assert member.user_id == user_id


@then('adding member "{user_id}" again raises TeamMemberExistsError')
def step_then_add_duplicate_raises(context: Context, user_id: str) -> None:
    raised = False
    try:
        context.collab_service.add_member(
            user_id=user_id,
            display_name="Dup",
            email="dup@example.com",
            role=TeamRole.MEMBER,
        )
    except TeamMemberExistsError:
        raised = True
    assert raised, "Expected TeamMemberExistsError"


@when('I remove member "{user_id}"')
def step_when_remove_member(context: Context, user_id: str) -> None:
    context.collab_service.remove_member(user_id)


@then('removing member "{user_id}" raises TeamMemberNotFoundError')
def step_then_remove_not_found_raises(context: Context, user_id: str) -> None:
    raised = False
    try:
        context.collab_service.remove_member(user_id)
    except TeamMemberNotFoundError:
        raised = True
    assert raised, "Expected TeamMemberNotFoundError"


@when('I update member "{user_id}" role to "{role}"')
def step_when_update_role(context: Context, user_id: str, role: str) -> None:
    context.collab_service.update_member_role(user_id, TeamRole(role))


@then('member "{user_id}" should have role "{role}"')
def step_then_member_has_role(context: Context, user_id: str, role: str) -> None:
    member = context.collab_service.get_member(user_id)
    assert member.role == TeamRole(role), f"Expected role {role}, got {member.role}"


# ── TeamCollaborationService — permission checks ──────────────────


@then('member "{user_id}" should have "{permission}" team permission')
def step_then_service_member_has_perm(
    context: Context, user_id: str, permission: str
) -> None:
    result = context.collab_service.check_permission(
        user_id, TeamPermission(permission)
    )
    assert result is True, f"Expected {user_id} to have {permission}"


@then('member "{user_id}" should not have "{permission}" team permission')
def step_then_service_member_no_perm(
    context: Context, user_id: str, permission: str
) -> None:
    result = context.collab_service.check_permission(
        user_id, TeamPermission(permission)
    )
    assert result is False, f"Expected {user_id} NOT to have {permission}"


@then('enforcing "{permission}" on "{user_id}" raises PermissionError')
def step_then_enforce_raises(context: Context, permission: str, user_id: str) -> None:
    raised = False
    try:
        context.collab_service.enforce_permission(user_id, TeamPermission(permission))
    except PermissionError:
        raised = True
    assert raised, "Expected PermissionError"


# ── TeamCollaborationService — session management ─────────────────


@when('I register a session for "{user_id}" on project "{project_id}"')
def step_when_register_service_session(
    context: Context, user_id: str, project_id: str
) -> None:
    session = context.collab_service.register_session(
        user_id=user_id,
        project_id=project_id,
    )
    context.last_session = session


@then('the project "{project_id}" should have {count:d} active sessions')
def step_then_project_active_sessions(
    context: Context, project_id: str, count: int
) -> None:
    sessions = context.collab_service.get_active_sessions_for_project(project_id)
    assert len(sessions) == count, (
        f"Expected {count} active sessions for '{project_id}', got {len(sessions)}"
    )


@when("I unregister the last session")
def step_when_unregister_last_session(context: Context) -> None:
    context.collab_service.unregister_session(context.last_session.session_id)


# ── TeamCollaborationService — conflict resolution ─────────────────


@when('I create a version stamp for "{resource_id}" by "{user_id}"')
def step_when_create_stamp(context: Context, resource_id: str, user_id: str) -> None:
    context.collab_service.create_version_stamp(resource_id, user_id)


@then('the version stamp for "{resource_id}" should have version {version:d}')
def step_then_stamp_has_version(
    context: Context, resource_id: str, version: int
) -> None:
    stamp = context.collab_service.get_version_stamp(resource_id)
    assert stamp is not None, f"No stamp for '{resource_id}'"
    assert stamp.version == version, f"Expected version {version}, got {stamp.version}"


@when('I update version for "{resource_id}" expecting {version:d} by "{user_id}"')
def step_when_update_version(
    context: Context, resource_id: str, version: int, user_id: str
) -> None:
    context.last_conflict_result = context.collab_service.update_version(
        resource_id=resource_id,
        expected_version=version,
        user_id=user_id,
    )


@then(
    'updating "{resource_id}" expecting {version:d} by "{user_id}" '
    "with reject raises VersionConflictError"
)
def step_then_update_reject_raises(
    context: Context, resource_id: str, version: int, user_id: str
) -> None:
    raised = False
    try:
        context.collab_service.update_version(
            resource_id=resource_id,
            expected_version=version,
            user_id=user_id,
            strategy=ConflictResolutionStrategy.REJECT,
        )
    except VersionConflictError:
        raised = True
    assert raised, "Expected VersionConflictError"


@when(
    'I update "{resource_id}" expecting {version:d} by "{user_id}" '
    "with last_writer_wins"
)
def step_when_update_lww(
    context: Context, resource_id: str, version: int, user_id: str
) -> None:
    context.last_conflict_result = context.collab_service.update_version(
        resource_id=resource_id,
        expected_version=version,
        user_id=user_id,
        strategy=ConflictResolutionStrategy.LAST_WRITER_WINS,
    )


@when('I update "{resource_id}" expecting {version:d} by "{user_id}" with merge')
def step_when_update_merge(
    context: Context, resource_id: str, version: int, user_id: str
) -> None:
    context.last_conflict_result = context.collab_service.update_version(
        resource_id=resource_id,
        expected_version=version,
        user_id=user_id,
        strategy=ConflictResolutionStrategy.MERGE,
    )


@then("the conflict result should be detected and resolved")
def step_then_conflict_detected_resolved(context: Context) -> None:
    result = context.last_conflict_result
    assert result.detected is True, "Expected conflict detected"
    assert result.resolved is True, "Expected conflict resolved"


@then('detecting conflict on "{resource_id}" expecting {version:d} should return True')
def step_then_detect_conflict_true(
    context: Context, resource_id: str, version: int
) -> None:
    result = context.collab_service.detect_conflict(resource_id, version)
    assert result is True, "Expected conflict detected"


@then('detecting conflict on "{resource_id}" expecting {version:d} should return False')
def step_then_detect_conflict_false(
    context: Context, resource_id: str, version: int
) -> None:
    result = context.collab_service.detect_conflict(resource_id, version)
    assert result is False, "Expected no conflict"


# ── Error hierarchy ────────────────────────────────────────────────


@then("TeamMemberNotFoundError should be a subclass of TeamCollaborationError")
def step_then_not_found_is_subclass(context: Context) -> None:
    assert issubclass(TeamMemberNotFoundError, TeamCollaborationError)


@then("TeamMemberExistsError should be a subclass of TeamCollaborationError")
def step_then_exists_is_subclass(context: Context) -> None:
    assert issubclass(TeamMemberExistsError, TeamCollaborationError)


@then("VersionConflictError should be a subclass of TeamCollaborationError")
def step_then_conflict_is_subclass(context: Context) -> None:
    assert issubclass(VersionConflictError, TeamCollaborationError)


# ── Service edge-case validations ──────────────────────────────────


@given("a fresh TeamCollaborationService")
def step_fresh_service(context: Context) -> None:
    context.service = TeamCollaborationService()


@then("the service session_registry should be a SessionRegistry instance")
def step_check_session_registry_type(context: Context) -> None:
    assert isinstance(context.service.session_registry, SessionRegistry)


@when("I try to add a member with empty user_id via service")
def step_add_member_empty_uid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.add_member(
            user_id="", display_name="X", email="x@x.com", role=TeamRole.MEMBER
        )
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to add a member with empty display_name via service")
def step_add_member_empty_name(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.add_member(
            user_id="u1", display_name="", email="x@x.com", role=TeamRole.MEMBER
        )
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to add a member with empty email via service")
def step_add_member_empty_email(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.add_member(
            user_id="u1", display_name="X", email="", role=TeamRole.MEMBER
        )
    except ValueError as exc:
        context.caught_exception = exc


@then('a collab ValueError should be raised mentioning "{fragment}"')
def step_check_value_error_msg(context: Context, fragment: str) -> None:
    assert context.caught_exception is not None, "Expected ValueError"
    assert isinstance(context.caught_exception, ValueError)
    assert fragment in str(context.caught_exception), (
        f"Expected '{fragment}' in '{context.caught_exception}'"
    )


@when("I try to remove a member with empty user_id via service")
def step_remove_member_empty_uid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.remove_member(user_id="")
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to get a member with empty user_id via service")
def step_get_member_empty_uid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.get_member(user_id="")
    except ValueError as exc:
        context.caught_exception = exc


@when('I try to get member "{uid}" via service')
def step_get_member_missing(context: Context, uid: str) -> None:
    context.caught_exception = None
    try:
        context.service.get_member(user_id=uid)
    except TeamMemberNotFoundError as exc:
        context.caught_exception = exc


@then("a TeamMemberNotFoundError should be raised")
def step_check_not_found_error(context: Context) -> None:
    assert context.caught_exception is not None, "Expected TeamMemberNotFoundError"
    assert isinstance(context.caught_exception, TeamMemberNotFoundError)


@when("I try to update role with empty user_id via service")
def step_update_role_empty_uid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.update_member_role(user_id="", new_role=TeamRole.ADMIN)
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to register session with empty user_id via service")
def step_register_session_empty_uid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.register_session(user_id="", project_id="p1")
    except ValueError as exc:
        context.caught_exception = exc


@given('a team member "{uid}" with role "{role}"')
def step_add_team_member(context: Context, uid: str, role: str) -> None:
    context.service.add_member(
        user_id=uid,
        display_name=f"User {uid}",
        email=f"{uid}@test.com",
        role=TeamRole(role),
    )


@when('I try to register session with empty project_id for user "{uid}"')
def step_register_session_empty_pid(context: Context, uid: str) -> None:
    context.caught_exception = None
    try:
        context.service.register_session(user_id=uid, project_id="")
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to unregister session with empty session_id")
def step_unregister_session_empty_sid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.unregister_session(session_id="")
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to get sessions for empty project_id")
def step_get_sessions_empty_pid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.get_active_sessions_for_project(project_id="")
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to create version stamp with empty resource_id")
def step_create_stamp_empty_rid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.create_version_stamp(resource_id="", user_id="u1")
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to create version stamp with empty user_id")
def step_create_stamp_empty_uid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.create_version_stamp(resource_id="r1", user_id="")
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to get version stamp with empty resource_id")
def step_get_stamp_empty_rid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.get_version_stamp(resource_id="")
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to update version with empty resource_id")
def step_update_version_empty_rid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.update_version(resource_id="", expected_version=1, user_id="u1")
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to update version with empty user_id")
def step_update_version_empty_uid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.update_version(resource_id="r1", expected_version=1, user_id="")
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to update version with expected_version 0")
def step_update_version_zero(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.update_version(
            resource_id="r1", expected_version=0, user_id="u1"
        )
    except ValueError as exc:
        context.caught_exception = exc


@when(
    'I update version for untracked resource "{rid}" by user "{uid}" expecting version {ver:d}'
)
def step_update_version_untracked(
    context: Context, rid: str, uid: str, ver: int
) -> None:
    context.conflict_result = context.service.update_version(
        resource_id=rid, expected_version=ver, user_id=uid
    )


@then("the result should indicate no conflict detected")
def step_check_no_conflict(context: Context) -> None:
    assert not context.conflict_result.detected, "Expected no conflict"


@then('the result details should contain "{text}"')
def step_check_result_details(context: Context, text: str) -> None:
    assert text in (context.conflict_result.details or ""), (
        f"Expected '{text}' in '{context.conflict_result.details}'"
    )


@when("I try to detect conflict with empty resource_id")
def step_detect_conflict_empty_rid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.detect_conflict(resource_id="", expected_version=1)
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to detect conflict with expected_version 0")
def step_detect_conflict_zero(context: Context) -> None:
    context.caught_exception = None
    try:
        context.service.detect_conflict(resource_id="r1", expected_version=0)
    except ValueError as exc:
        context.caught_exception = exc


@when('I detect conflict for untracked resource "{rid}" expecting version {ver:d}')
def step_detect_conflict_untracked(context: Context, rid: str, ver: int) -> None:
    context.conflict_bool = context.service.detect_conflict(
        resource_id=rid, expected_version=ver
    )


@then("the conflict result should be False")
def step_check_conflict_false(context: Context) -> None:
    assert context.conflict_bool is False


# ── Domain model edge-case validations ─────────────────────────────


@when("I try to create VersionConflictError with expected_version -1")
def step_vce_negative_expected(context: Context) -> None:
    context.caught_exception = None
    try:
        VersionConflictError(resource_id="r1", expected_version=-1, actual_version=1)
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to create VersionConflictError with actual_version -1")
def step_vce_negative_actual(context: Context) -> None:
    context.caught_exception = None
    try:
        VersionConflictError(resource_id="r1", expected_version=1, actual_version=-1)
    except ValueError as exc:
        context.caught_exception = exc


@given("a fresh SessionRegistry")
def step_fresh_registry(context: Context) -> None:
    context.registry = SessionRegistry()


@when("I try to register a session with empty session_id")
def step_register_empty_sid(context: Context) -> None:
    context.caught_exception = None
    try:
        session = UserSession(
            session_id="",
            user_id="u1",
            display_name="User 1",
            project_id="p1",
        )
        context.registry.register(session)
    except (ValueError, ValidationError) as exc:
        context.caught_exception = exc


@when("I try to unregister a session with empty session_id")
def step_unregister_empty_sid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.registry.unregister("")
    except ValueError as exc:
        context.caught_exception = exc


@when('I unregister session "{sid}"')
def step_unregister_unknown(context: Context, sid: str) -> None:
    context.unregister_result = context.registry.unregister(sid)


@then("the unregister result should be False")
def step_check_unregister_false(context: Context) -> None:
    assert context.unregister_result is False


@when("I try to get session with empty session_id")
def step_get_session_empty_sid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.registry.get("")
    except ValueError as exc:
        context.caught_exception = exc


@given('a registered session "{sid}" for user "{uid}" in project "{pid}"')
def step_register_session_in_registry(
    context: Context, sid: str, uid: str, pid: str
) -> None:
    session = UserSession(
        session_id=sid,
        user_id=uid,
        display_name=f"User {uid}",
        project_id=pid,
    )
    context.registry.register(session)


@when('I deactivate session "{sid}" in the registry')
def step_deactivate_session(context: Context, sid: str) -> None:
    session = context.registry.get(sid)
    session.deactivate()


@then("get_active_sessions should return {count:d} session")
def step_check_active_count(context: Context, count: int) -> None:
    active = context.registry.get_active_sessions()
    assert len(active) == count, f"Expected {count} active sessions, got {len(active)}"


@when("I try to get sessions for empty user_id")
def step_get_sessions_empty_uid(context: Context) -> None:
    context.caught_exception = None
    try:
        context.registry.get_sessions_for_user("")
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to get sessions for empty project_id via registry")
def step_get_sessions_empty_pid_registry(context: Context) -> None:
    context.caught_exception = None
    try:
        context.registry.get_sessions_for_project("")
    except ValueError as exc:
        context.caught_exception = exc
