"""Step definitions for the permission system feature.

Tests the permission domain model, PermissionService in local and
server modes, the enforce_permission decorator, and role binding
management.
"""

from __future__ import annotations

import os

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

from cleveragents.application.services.permission_service import (
    PermissionService,
    enforce_permission,
    set_default_permission_service,
)
from cleveragents.domain.models.core.permission import (
    DEFAULT_LOCAL_ROLE_MAPPING,
    ROLE_PERMISSIONS,
    PermissionAction,
    PermissionCheck,
    PermissionPolicy,
    PermissionRole,
    PermissionScope,
    RoleBinding,
)

# ── Helpers ───────────────────────────────────────────────────────

_SERVER_MODE_ENV = "CLEVERAGENTS_SERVER_MODE"


def _set_local_mode() -> None:
    os.environ.pop(_SERVER_MODE_ENV, None)


def _set_server_mode() -> None:
    os.environ[_SERVER_MODE_ENV] = "true"


# ── Enum steps ────────────────────────────────────────────────────


@given("the PermissionRole enum")
def step_given_permission_role_enum(context: Context) -> None:
    context.enum_values = [e.value for e in PermissionRole]


@given("the PermissionScope enum")
def step_given_permission_scope_enum(context: Context) -> None:
    context.enum_values = [e.value for e in PermissionScope]


@given("the PermissionAction enum")
def step_given_permission_action_enum(context: Context) -> None:
    context.enum_values = [e.value for e in PermissionAction]


@then('the enum should contain "{value}"')
def step_enum_should_contain(context: Context, value: str) -> None:
    assert value in context.enum_values, f"Expected '{value}' in {context.enum_values}"


# ── ROLE_PERMISSIONS ──────────────────────────────────────────────


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


@then('the "{role}" role should have {count:d} allowed actions')
def step_role_has_n_actions(context: Context, role: str, count: int) -> None:
    perm_role = PermissionRole(role)
    actions = context.role_permissions[perm_role]
    assert len(actions) == count, (
        f"Expected {count} actions for {role}, got {len(actions)}"
    )


# ── RoleBinding model ────────────────────────────────────────────


@given(
    'a role binding for principal "{principal}" with role "{role}" '
    'on "{scope}" scope "{scope_id}"'
)
def step_given_role_binding(
    context: Context, principal: str, role: str, scope: str, scope_id: str
) -> None:
    binding = RoleBinding(
        principal=principal,
        role=PermissionRole(role),
        scope=PermissionScope(scope),
        scope_id=scope_id,
    )
    context.binding = binding
    # Also register on service if present
    svc: PermissionService | None = getattr(context, "permission_service", None)
    if svc is not None:
        svc.add_binding(binding)


@then('the binding principal should be "{expected}"')
def step_binding_principal(context: Context, expected: str) -> None:
    assert context.binding.principal == expected


@then('the binding role should be "{expected}"')
def step_binding_role(context: Context, expected: str) -> None:
    assert context.binding.role == expected


@then('the binding scope should be "{expected}"')
def step_binding_scope(context: Context, expected: str) -> None:
    assert context.binding.scope == expected


@then('the binding scope_id should be "{expected}"')
def step_binding_scope_id(context: Context, expected: str) -> None:
    assert context.binding.scope_id == expected


# ── PermissionPolicy ─────────────────────────────────────────────


@given("a default PermissionPolicy")
def step_given_default_policy(context: Context) -> None:
    context.policy = PermissionPolicy()


@given(
    'a PermissionPolicy with an override for "{principal}" as "{role}" '
    'on "{scope}" scope "{scope_id}"'
)
def step_given_policy_with_override(
    context: Context, principal: str, role: str, scope: str, scope_id: str
) -> None:
    override = RoleBinding(
        principal=principal,
        role=PermissionRole(role),
        scope=PermissionScope(scope),
        scope_id=scope_id,
    )
    context.policy = PermissionPolicy(allow_overrides=[override])


@then("default_deny should be true")
def step_default_deny_true(context: Context) -> None:
    assert context.policy.default_deny is True


@then("allow_overrides should be empty")
def step_overrides_empty(context: Context) -> None:
    assert len(context.policy.allow_overrides) == 0


@then("the policy should have {count:d} allow override")
def step_policy_override_count(context: Context, count: int) -> None:
    assert len(context.policy.allow_overrides) == count


# ── PermissionService — local mode ────────────────────────────────


@given("a PermissionService in local mode")
def step_given_service_local(context: Context) -> None:
    _set_local_mode()
    context.permission_service = PermissionService()


@given("a PermissionService in server mode with no bindings")
def step_given_service_server_no_bindings(context: Context) -> None:
    _set_server_mode()
    context.permission_service = PermissionService()


@given("a PermissionService in server mode")
def step_given_service_server(context: Context) -> None:
    _set_server_mode()
    context.permission_service = PermissionService()


@given("a PermissionService in server mode with default_deny disabled")
def step_given_service_server_no_deny(context: Context) -> None:
    _set_server_mode()
    policy = PermissionPolicy(default_deny=False)
    context.permission_service = PermissionService(policy=policy)


@given(
    'a policy override for "{principal}" as "{role}" on "{scope}" scope "{scope_id}"'
)
def step_given_policy_override_on_service(
    context: Context, principal: str, role: str, scope: str, scope_id: str
) -> None:
    override = RoleBinding(
        principal=principal,
        role=PermissionRole(role),
        scope=PermissionScope(scope),
        scope_id=scope_id,
    )
    policy = PermissionPolicy(allow_overrides=[override])
    context.permission_service = PermissionService(policy=policy)


# ── Permission check ─────────────────────────────────────────────


@when(
    'I check permission for "{principal}" to "{action}" on "{scope}" scope "{scope_id}"'
)
def step_check_permission(
    context: Context, principal: str, action: str, scope: str, scope_id: str
) -> None:
    svc: PermissionService = context.permission_service
    context.permission_result = svc.check_permission(
        principal=principal,
        action=PermissionAction(action),
        scope=PermissionScope(scope),
        scope_id=scope_id,
    )


@then("the permission check result should be true")
def step_check_result_true(context: Context) -> None:
    assert context.permission_result.result is True, (
        f"Expected True, got False: {context.permission_result.reason}"
    )


@then("the permission check result should be false")
def step_check_result_false(context: Context) -> None:
    assert context.permission_result.result is False, (
        f"Expected False, got True: {context.permission_result.reason}"
    )


@then('the reason should contain "{fragment}"')
def step_reason_contains(context: Context, fragment: str) -> None:
    assert fragment in context.permission_result.reason, (
        f"Expected '{fragment}' in '{context.permission_result.reason}'"
    )


# ── DEFAULT_LOCAL_ROLE_MAPPING ────────────────────────────────────


@given("the DEFAULT_LOCAL_ROLE_MAPPING constant")
def step_given_default_local_mapping(context: Context) -> None:
    context.local_mapping = DEFAULT_LOCAL_ROLE_MAPPING


@then('the wildcard entry should map to "{expected}"')
def step_wildcard_maps_to(context: Context, expected: str) -> None:
    assert context.local_mapping["*"] == expected


# ── PermissionCheck model ─────────────────────────────────────────


@given(
    'a PermissionCheck for "{principal}" action "{action}" scope "{scope}" '
    'scope_id "{scope_id}" result true reason "{reason}"'
)
def step_given_permission_check_model(
    context: Context,
    principal: str,
    action: str,
    scope: str,
    scope_id: str,
    reason: str,
) -> None:
    context.check_model = PermissionCheck(
        principal=principal,
        action=PermissionAction(action),
        scope=PermissionScope(scope),
        scope_id=scope_id,
        result=True,
        reason=reason,
    )


@then('the check principal should be "{expected}"')
def step_check_principal(context: Context, expected: str) -> None:
    assert context.check_model.principal == expected


@then('the check action should be "{expected}"')
def step_check_action(context: Context, expected: str) -> None:
    assert context.check_model.action == expected


@then('the check scope should be "{expected}"')
def step_check_scope(context: Context, expected: str) -> None:
    assert context.check_model.scope == expected


@then('the check scope_id should be "{expected}"')
def step_check_scope_id(context: Context, expected: str) -> None:
    assert context.check_model.scope_id == expected


@then("the check result should be true")
def step_check_model_result_true(context: Context) -> None:
    assert context.check_model.result is True


@then('the check reason should be "{expected}"')
def step_check_reason(context: Context, expected: str) -> None:
    assert context.check_model.reason == expected


# ── enforce_permission decorator ──────────────────────────────────


@given(
    "a function decorated with enforce_permission for "
    '"{action}" on "{scope}" scope "{scope_id}"'
)
def step_given_decorated_fn(
    context: Context, action: str, scope: str, scope_id: str
) -> None:
    context.enforce_action = action
    context.enforce_scope = scope
    context.enforce_scope_id = scope_id


@when("I call the decorated function in local mode")
def step_call_decorated_local(context: Context) -> None:
    _set_local_mode()

    @enforce_permission(
        PermissionAction(context.enforce_action),
        PermissionScope(context.enforce_scope),
        context.enforce_scope_id,
    )
    def _guarded() -> str:
        return "ok"

    try:
        context.decorated_result = _guarded()
        context.decorated_error = None
    except PermissionError as exc:
        context.decorated_result = None
        context.decorated_error = exc


@when("I call the decorated function in server mode with no bindings")
def step_call_decorated_server(context: Context) -> None:
    _set_server_mode()

    @enforce_permission(
        PermissionAction(context.enforce_action),
        PermissionScope(context.enforce_scope),
        context.enforce_scope_id,
    )
    def _guarded() -> str:
        return "ok"

    try:
        context.decorated_result = _guarded()
        context.decorated_error = None
    except PermissionError as exc:
        context.decorated_result = None
        context.decorated_error = exc


@then("it should succeed without raising")
def step_no_error(context: Context) -> None:
    assert context.decorated_error is None
    assert context.decorated_result == "ok"


@then("it should raise a PermissionError")
def step_permission_error_raised(context: Context) -> None:
    assert context.decorated_error is not None
    assert isinstance(context.decorated_error, PermissionError)


# ── Binding management ────────────────────────────────────────────


@given('I add a binding for "{principal}" as "{role}" on "{scope}" scope "{scope_id}"')
def step_add_binding(
    context: Context, principal: str, role: str, scope: str, scope_id: str
) -> None:
    binding = RoleBinding(
        principal=principal,
        role=PermissionRole(role),
        scope=PermissionScope(scope),
        scope_id=scope_id,
    )
    context.permission_service.add_binding(binding)


@when(
    'I remove the binding for "{principal}" as "{role}" on "{scope}" scope "{scope_id}"'
)
def step_remove_binding(
    context: Context, principal: str, role: str, scope: str, scope_id: str
) -> None:
    binding = RoleBinding(
        principal=principal,
        role=PermissionRole(role),
        scope=PermissionScope(scope),
        scope_id=scope_id,
    )
    context.remove_result = context.permission_service.remove_binding(binding)


@when(
    'I try to remove a binding for "{principal}" as "{role}" '
    'on "{scope}" scope "{scope_id}"'
)
def step_try_remove_binding(
    context: Context, principal: str, role: str, scope: str, scope_id: str
) -> None:
    binding = RoleBinding(
        principal=principal,
        role=PermissionRole(role),
        scope=PermissionScope(scope),
        scope_id=scope_id,
    )
    context.remove_result = context.permission_service.remove_binding(binding)


@then("the remove result should be false")
def step_remove_result_false(context: Context) -> None:
    assert context.remove_result is False


# ── get_role_bindings ─────────────────────────────────────────────


@when('I get role bindings for "{principal}" on "{scope}" scope "{scope_id}"')
def step_get_role_bindings(
    context: Context, principal: str, scope: str, scope_id: str
) -> None:
    svc: PermissionService = context.permission_service
    context.fetched_bindings = svc.get_role_bindings(
        principal=principal,
        scope=PermissionScope(scope),
        scope_id=scope_id,
    )


@then("I should receive {count:d} binding")
def step_binding_count_singular(context: Context, count: int) -> None:
    assert len(context.fetched_bindings) == count, (
        f"Expected {count} bindings, got {len(context.fetched_bindings)}"
    )


@then("I should receive {count:d} bindings")
def step_binding_count_plural(context: Context, count: int) -> None:
    assert len(context.fetched_bindings) == count, (
        f"Expected {count} bindings, got {len(context.fetched_bindings)}"
    )


@then('the first binding role should be "{expected}"')
def step_first_binding_role(context: Context, expected: str) -> None:
    assert context.fetched_bindings[0].role == expected


# ── Duplicate binding prevention ──────────────────────────────────


@when(
    'I try to add a duplicate binding for "{principal}" as "{role}" '
    'on "{scope}" scope "{scope_id}"'
)
def step_try_add_duplicate_binding(
    context: Context, principal: str, role: str, scope: str, scope_id: str
) -> None:
    binding = RoleBinding(
        principal=principal,
        role=PermissionRole(role),
        scope=PermissionScope(scope),
        scope_id=scope_id,
    )
    context.add_result = context.permission_service.add_binding(binding)


@then("the add result should be false")
def step_add_result_false(context: Context) -> None:
    assert context.add_result is False


# ── RoleBinding validation ────────────────────────────────────────


@when("I try to create a role binding with empty principal")
def step_try_create_binding_empty_principal(context: Context) -> None:
    context.permission_error = None
    try:
        RoleBinding(
            principal="",
            role=PermissionRole.VIEWER,
            scope=PermissionScope.PROJECT,
            scope_id="proj-1",
        )
    except ValidationError as exc:
        context.permission_error = exc


@when("I try to create a role binding with empty scope_id")
def step_try_create_binding_empty_scope_id(context: Context) -> None:
    context.permission_error = None
    try:
        RoleBinding(
            principal="alice",
            role=PermissionRole.VIEWER,
            scope=PermissionScope.PROJECT,
            scope_id="",
        )
    except ValidationError as exc:
        context.permission_error = exc


@then("a permission validation error should be raised")
def step_permission_validation_error(context: Context) -> None:
    assert context.permission_error is not None, "Expected a ValidationError"
    assert isinstance(context.permission_error, ValidationError)


# ── PermissionCheck with result=False ─────────────────────────────


@given(
    'a PermissionCheck for "{principal}" action "{action}" scope "{scope}" '
    'scope_id "{scope_id}" result false reason "{reason}"'
)
def step_given_permission_check_model_false(
    context: Context,
    principal: str,
    action: str,
    scope: str,
    scope_id: str,
    reason: str,
) -> None:
    context.check_model = PermissionCheck(
        principal=principal,
        action=PermissionAction(action),
        scope=PermissionScope(scope),
        scope_id=scope_id,
        result=False,
        reason=reason,
    )


@then("the check result should be false")
def step_check_model_result_false(context: Context) -> None:
    assert context.check_model.result is False


# ── enforce_permission with pre-loaded service ────────────────────


@given(
    'a pre-loaded permission service with "{principal}" as "{role}" '
    'on "{scope}" scope "{scope_id}" for action "{action}"'
)
def step_given_decorated_fn_preloaded(
    context: Context,
    principal: str,
    role: str,
    scope: str,
    scope_id: str,
    action: str,
) -> None:
    binding = RoleBinding(
        principal=principal,
        role=PermissionRole(role),
        scope=PermissionScope(scope),
        scope_id=scope_id,
    )
    context.preloaded_service = PermissionService(bindings=[binding])
    context.preloaded_principal = principal
    context.enforce_action = action
    context.enforce_scope = scope
    context.enforce_scope_id = scope_id


@when("I call the pre-loaded decorated function in server mode")
def step_call_preloaded_decorated_server(context: Context) -> None:
    _set_server_mode()

    @enforce_permission(
        PermissionAction(context.enforce_action),
        PermissionScope(context.enforce_scope),
        context.enforce_scope_id,
        principal=context.preloaded_principal,
        service=context.preloaded_service,
    )
    def _guarded() -> str:
        return "ok"

    try:
        context.decorated_result = _guarded()
        context.decorated_error = None
    except PermissionError as exc:
        context.decorated_result = None
        context.decorated_error = exc


# ── enforce_permission with module-level default service ──────────


@given(
    'a module-level default service with "{principal}" as "{role}" '
    'on "{scope}" scope "{scope_id}"'
)
def step_given_module_default_service(
    context: Context,
    principal: str,
    role: str,
    scope: str,
    scope_id: str,
) -> None:
    binding = RoleBinding(
        principal=principal,
        role=PermissionRole(role),
        scope=PermissionScope(scope),
        scope_id=scope_id,
    )
    svc = PermissionService(bindings=[binding])
    set_default_permission_service(svc)
    context.default_svc_principal = principal


@when("I call the decorated function in server mode via module default")
def step_call_decorated_server_module_default(context: Context) -> None:
    _set_server_mode()

    @enforce_permission(
        PermissionAction(context.enforce_action),
        PermissionScope(context.enforce_scope),
        context.enforce_scope_id,
        principal=context.default_svc_principal,
    )
    def _guarded() -> str:
        return "ok"

    try:
        context.decorated_result = _guarded()
        context.decorated_error = None
    except PermissionError as exc:
        context.decorated_result = None
        context.decorated_error = exc
    finally:
        # Clean up module-level default
        set_default_permission_service(None)


# ── Input validation — check_permission ───────────────────────────


@when("I check permission with empty principal")
def step_check_permission_empty_principal(context: Context) -> None:
    context.value_error = None
    try:
        context.permission_service.check_permission(
            principal="",
            action=PermissionAction.READ,
            scope=PermissionScope.PROJECT,
            scope_id="proj-1",
        )
    except ValueError as exc:
        context.value_error = exc


@when("I check permission with whitespace-only principal")
def step_check_permission_whitespace_principal(context: Context) -> None:
    context.value_error = None
    try:
        context.permission_service.check_permission(
            principal="   ",
            action=PermissionAction.READ,
            scope=PermissionScope.PROJECT,
            scope_id="proj-1",
        )
    except ValueError as exc:
        context.value_error = exc


@when("I check permission with empty scope_id")
def step_check_permission_empty_scope_id(context: Context) -> None:
    context.value_error = None
    try:
        context.permission_service.check_permission(
            principal="alice",
            action=PermissionAction.READ,
            scope=PermissionScope.PROJECT,
            scope_id="",
        )
    except ValueError as exc:
        context.value_error = exc


@when("I check permission with whitespace-only scope_id")
def step_check_permission_whitespace_scope_id(context: Context) -> None:
    context.value_error = None
    try:
        context.permission_service.check_permission(
            principal="alice",
            action=PermissionAction.READ,
            scope=PermissionScope.PROJECT,
            scope_id="   ",
        )
    except ValueError as exc:
        context.value_error = exc


@then('a permission ValueError should mention "{fragment}"')
def step_permission_value_error_with_message(context: Context, fragment: str) -> None:
    assert context.value_error is not None, "Expected a ValueError"
    assert isinstance(context.value_error, ValueError)
    assert fragment in str(context.value_error), (
        f"Expected '{fragment}' in '{context.value_error}'"
    )


# ── Input validation — get_role_bindings ──────────────────────────


@when("I get role bindings with empty principal")
def step_get_bindings_empty_principal(context: Context) -> None:
    context.value_error = None
    try:
        context.permission_service.get_role_bindings(
            principal="",
            scope=PermissionScope.PROJECT,
            scope_id="proj-1",
        )
    except ValueError as exc:
        context.value_error = exc


@when("I get role bindings with whitespace-only scope_id")
def step_get_bindings_whitespace_scope_id(context: Context) -> None:
    context.value_error = None
    try:
        context.permission_service.get_role_bindings(
            principal="alice",
            scope=PermissionScope.PROJECT,
            scope_id="   ",
        )
    except ValueError as exc:
        context.value_error = exc
