"""Step definitions for AWS session building scenarios.

Tests _build_aws_session and CloudResourceHandler.resolve() with mocked boto3.
"""

from __future__ import annotations

import os
from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when  # type: ignore[attr-defined]

from cleveragents.resource.handlers.cloud import CloudResourceHandler
from cleveragents.resource.handlers.cloud_aws import (
    _BOTO3_AVAILABLE,
    _build_aws_session,
)
from features.steps.cloud_aws_helpers import (
    _clear_cloud_env,
    _make_mock_session,
    _make_resource,
    _restore_cloud_env,
)


@given("awssdk the cloud handler module is imported")
def step_module_imported(context: Any) -> None:
    """Ensure the cloud handler module is importable."""
    import cleveragents.resource.handlers.cloud as _mod

    context.cloud_module = _mod  # type: ignore[attr-defined]


@given("awssdk boto3 is not available")
def step_boto3_not_available(context: Any) -> None:
    """Simulate boto3 not being installed."""
    context.boto3_available = False  # type: ignore[attr-defined]
    context.saved_env = _clear_cloud_env()  # type: ignore[attr-defined]


@given("awssdk boto3 is available via mock")
def step_boto3_available_mock(context: Any) -> None:
    """Mark boto3 as available (mocked)."""
    context.boto3_available = True  # type: ignore[attr-defined]
    context.saved_env = _clear_cloud_env()  # type: ignore[attr-defined]
    os.environ["AWS_ACCESS_KEY_ID"] = "AKIAIOSFODNN7EXAMPLE"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
    os.environ["AWS_REGION"] = "us-east-1"


@given('awssdk AWS STS returns account id "{account_id}"')
def step_sts_account_id(context: Any, account_id: str) -> None:
    """Configure mock STS to return a specific account ID."""
    context.mock_account_id = account_id  # type: ignore[attr-defined]


@given("awssdk AWS STS raises a ClientError")
def step_sts_client_error(context: Any) -> None:
    """Configure mock STS to raise a ClientError."""
    context.sts_error = RuntimeError("AWS ClientError: InvalidClientTokenId")  # type: ignore[attr-defined]


@given('awssdk a valid AWS cloud resource of type "{type_name}"')
def step_aws_resource_type(context: Any, type_name: str) -> None:
    """Create an AWS resource of the given type."""
    context.cloud_resource = _make_resource(  # type: ignore[attr-defined]
        type_name,
        properties={
            "access-key-id": "AKIAIOSFODNN7EXAMPLE",
            "secret-access-key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
            "region": "us-east-1",
        },
    )


@given("awssdk a valid GCP cloud resource with credentials")
def step_gcp_resource_with_creds(context: Any) -> None:
    """Create a GCP resource with credentials."""
    context.cloud_resource = _make_resource(  # type: ignore[attr-defined]
        "gcp",
        properties={
            "service-account-json-path": "/path/to/sa.json",
            "project-id": "my-project",
        },
    )


@given("awssdk a valid Azure cloud resource with credentials")
def step_azure_resource_with_creds(context: Any) -> None:
    """Create an Azure resource with credentials."""
    context.cloud_resource = _make_resource(  # type: ignore[attr-defined]
        "azure",
        properties={
            "subscription-id": "sub-123",
            "tenant-id": "tenant-456",
            "client-id": "client-789",
            "client-secret": "secret-abc",
        },
    )


@when("awssdk I try to build an AWS session with valid credentials")
def step_try_build_session_no_boto3(context: Any) -> None:
    """Try to build an AWS session when boto3 is unavailable."""
    context.raised_error = None  # type: ignore[attr-defined]
    context.raised_error_type = None  # type: ignore[attr-defined]
    with (
        patch("cleveragents.resource.handlers.cloud_aws._BOTO3_AVAILABLE", False),
        patch("cleveragents.resource.handlers.cloud_aws.boto3", None),
    ):
        try:
            _build_aws_session(
                {
                    "access-key-id": "AKIAIOSFODNN7EXAMPLE",
                    "secret-access-key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
                }
            )
        except ImportError as exc:
            context.raised_error = exc  # type: ignore[attr-defined]
            context.raised_error_type = "ImportError"  # type: ignore[attr-defined]


@when("awssdk I build an AWS session with explicit credentials")
def step_build_session_explicit(context: Any) -> None:
    """Build an AWS session with explicit credentials using mocked boto3."""
    context.raised_error = None  # type: ignore[attr-defined]
    mock_boto3 = MagicMock()
    mock_session = MagicMock()
    mock_boto3.Session.return_value = mock_session
    with (
        patch("cleveragents.resource.handlers.cloud_aws.boto3", mock_boto3),
        patch("cleveragents.resource.handlers.cloud_aws._BOTO3_AVAILABLE", True),
    ):
        result = _build_aws_session(
            {
                "access-key-id": "AKIAIOSFODNN7EXAMPLE",
                "secret-access-key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
                "region": "us-east-1",
            }
        )
    context.session_result = result  # type: ignore[attr-defined]
    context.mock_boto3 = mock_boto3  # type: ignore[attr-defined]


@when('awssdk I build an AWS session with a profile name "{profile}"')
def step_build_session_profile(context: Any, profile: str) -> None:
    """Build an AWS session with a profile name."""
    context.raised_error = None  # type: ignore[attr-defined]
    mock_boto3 = MagicMock()
    mock_session = MagicMock()
    mock_boto3.Session.return_value = mock_session
    with (
        patch("cleveragents.resource.handlers.cloud_aws.boto3", mock_boto3),
        patch("cleveragents.resource.handlers.cloud_aws._BOTO3_AVAILABLE", True),
    ):
        result = _build_aws_session({"profile": profile})
    context.session_result = result  # type: ignore[attr-defined]
    context.mock_boto3 = mock_boto3  # type: ignore[attr-defined]
    context.expected_profile = profile  # type: ignore[attr-defined]


@when("awssdk I call resolve on the cloud handler with mocked boto3")
def step_resolve_with_mock(context: Any) -> None:
    """Call resolve on CloudResourceHandler with mocked boto3."""
    from cleveragents.domain.models.core.resource import Resource

    handler = CloudResourceHandler()
    resource: Resource = context.cloud_resource  # type: ignore[attr-defined]
    mock_manager = MagicMock()

    account_id = getattr(context, "mock_account_id", "123456789012")
    sts_error = getattr(context, "sts_error", None)
    mock_session = _make_mock_session(account_id=account_id, sts_error=sts_error)

    context.raised_error = None  # type: ignore[attr-defined]
    context.raised_error_type = None  # type: ignore[attr-defined]
    context.bound_resource = None  # type: ignore[attr-defined]

    mock_boto3 = MagicMock()
    mock_boto3.Session.return_value = mock_session

    with (
        patch("cleveragents.resource.handlers.cloud_aws.boto3", mock_boto3),
        patch("cleveragents.resource.handlers.cloud_aws._BOTO3_AVAILABLE", True),
        patch("cleveragents.resource.handlers.cloud._BOTO3_AVAILABLE", True),
    ):
        try:
            result = handler.resolve(
                resource=resource,
                plan_id="PLAN-TEST-001",
                slot_name="cloud-slot",
                sandbox_manager=mock_manager,
            )
            context.bound_resource = result  # type: ignore[attr-defined]
        except (ValueError, NotImplementedError, ImportError) as exc:
            context.raised_error = exc  # type: ignore[attr-defined]
            context.raised_error_type = type(exc).__name__  # type: ignore[attr-defined]
    saved = getattr(context, "saved_env", {})
    _restore_cloud_env(saved)


@when("awssdk I call resolve on the cloud handler without boto3")
def step_resolve_without_boto3(context: Any) -> None:
    """Call resolve on CloudResourceHandler without boto3."""
    from cleveragents.domain.models.core.resource import Resource

    handler = CloudResourceHandler()
    resource: Resource = context.cloud_resource  # type: ignore[attr-defined]
    mock_manager = MagicMock()

    context.raised_error = None  # type: ignore[attr-defined]
    context.raised_error_type = None  # type: ignore[attr-defined]
    context.bound_resource = None  # type: ignore[attr-defined]

    with (
        patch("cleveragents.resource.handlers.cloud_aws._BOTO3_AVAILABLE", False),
        patch("cleveragents.resource.handlers.cloud_aws.boto3", None),
        patch("cleveragents.resource.handlers.cloud._BOTO3_AVAILABLE", False),
    ):
        try:
            result = handler.resolve(
                resource=resource,
                plan_id="PLAN-TEST-001",
                slot_name="cloud-slot",
                sandbox_manager=mock_manager,
            )
            context.bound_resource = result  # type: ignore[attr-defined]
        except (ValueError, NotImplementedError, ImportError) as exc:
            context.raised_error = exc  # type: ignore[attr-defined]
            context.raised_error_type = type(exc).__name__  # type: ignore[attr-defined]
    saved = getattr(context, "saved_env", {})
    _restore_cloud_env(saved)


@then("awssdk the boto3 availability flag should be a boolean")
def step_boto3_flag_bool(context: Any) -> None:
    """Check that _BOTO3_AVAILABLE is a boolean."""
    assert isinstance(_BOTO3_AVAILABLE, bool), (
        f"Expected bool, got {type(_BOTO3_AVAILABLE)}"
    )


@then('awssdk an ImportError should be raised mentioning "{text}"')
def step_import_error_raised(context: Any, text: str) -> None:
    """Check that an ImportError was raised with the expected text."""
    assert context.raised_error_type == "ImportError", (  # type: ignore[attr-defined]
        f"Expected ImportError, got {context.raised_error_type}: "  # type: ignore[attr-defined]
        f"{context.raised_error}"  # type: ignore[attr-defined]
    )
    msg = str(context.raised_error)  # type: ignore[attr-defined]
    assert text in msg, f"'{text}' not found in ImportError: {msg}"


@then("awssdk the session should be created successfully")
def step_session_created(context: Any) -> None:
    """Check that a session was created."""
    assert context.session_result is not None  # type: ignore[attr-defined]
    mock_boto3: MagicMock = context.mock_boto3  # type: ignore[attr-defined]
    assert mock_boto3.Session.called, "boto3.Session was not called"


@then('awssdk the session should be created with profile "{profile}"')
def step_session_created_with_profile(context: Any, profile: str) -> None:
    """Check that the session was created with the expected profile."""
    assert context.session_result is not None  # type: ignore[attr-defined]
    mock_boto3: MagicMock = context.mock_boto3  # type: ignore[attr-defined]
    call_kwargs = mock_boto3.Session.call_args[1]
    assert call_kwargs.get("profile_name") == profile, (
        f"Expected profile_name='{profile}', got {call_kwargs}"
    )


@then("awssdk a BoundResource should be returned")
def step_bound_resource_returned(context: Any) -> None:
    """Check that a BoundResource was returned."""
    from cleveragents.tool.context import BoundResource

    assert context.raised_error is None, (  # type: ignore[attr-defined]
        f"Expected BoundResource but got error: "
        f"{context.raised_error_type}: {context.raised_error}"  # type: ignore[attr-defined]
    )
    assert isinstance(context.bound_resource, BoundResource), (  # type: ignore[attr-defined]
        f"Expected BoundResource, got {type(context.bound_resource)}"  # type: ignore[attr-defined]
    )


@then('awssdk the BoundResource slot_name should be "{slot_name}"')
def step_bound_resource_slot(context: Any, slot_name: str) -> None:
    """Check BoundResource slot_name."""
    from cleveragents.tool.context import BoundResource

    br: BoundResource = context.bound_resource  # type: ignore[attr-defined]
    assert br.slot_name == slot_name, (
        f"Expected slot_name='{slot_name}', got '{br.slot_name}'"
    )


@then('awssdk the BoundResource resource_type should be "{resource_type}"')
def step_bound_resource_type(context: Any, resource_type: str) -> None:
    """Check BoundResource resource_type."""
    from cleveragents.tool.context import BoundResource

    br: BoundResource = context.bound_resource  # type: ignore[attr-defined]
    assert br.resource_type == resource_type, (
        f"Expected resource_type='{resource_type}', got '{br.resource_type}'"
    )


@then('awssdk the BoundResource sandbox_path should contain "{text}"')
def step_bound_resource_sandbox_path(context: Any, text: str) -> None:
    """Check BoundResource sandbox_path contains expected text."""
    from cleveragents.tool.context import BoundResource

    br: BoundResource = context.bound_resource  # type: ignore[attr-defined]
    assert br.sandbox_path is not None, "Expected sandbox_path to be set"
    assert text in br.sandbox_path, (
        f"Expected '{text}' in sandbox_path='{br.sandbox_path}'"
    )


@then('awssdk a ValueError should be raised mentioning "{text}"')
def step_value_error_raised(context: Any, text: str) -> None:
    """Check that a ValueError was raised with the expected text."""
    assert context.raised_error_type == "ValueError", (  # type: ignore[attr-defined]
        f"Expected ValueError, got {context.raised_error_type}: "  # type: ignore[attr-defined]
        f"{context.raised_error}"  # type: ignore[attr-defined]
    )
    msg = str(context.raised_error)  # type: ignore[attr-defined]
    assert text in msg, f"'{text}' not found in ValueError: {msg}"


@then('awssdk a NotImplementedError should be raised mentioning "{text}"')
def step_not_implemented_raised(context: Any, text: str) -> None:
    """Check that a NotImplementedError was raised with the expected text."""
    assert context.raised_error_type == "NotImplementedError", (  # type: ignore[attr-defined]
        f"Expected NotImplementedError, got {context.raised_error_type}: "  # type: ignore[attr-defined]
        f"{context.raised_error}"  # type: ignore[attr-defined]
    )
    msg = str(context.raised_error)  # type: ignore[attr-defined]
    assert text in msg, f"'{text}' not found in NotImplementedError: {msg}"


@then("awssdk a NotImplementedError should be raised")
def step_not_implemented_raised_any(context: Any) -> None:
    """Check that a NotImplementedError was raised."""
    assert context.raised_error_type == "NotImplementedError", (  # type: ignore[attr-defined]
        f"Expected NotImplementedError, got {context.raised_error_type}: "  # type: ignore[attr-defined]
        f"{context.raised_error}"  # type: ignore[attr-defined]
    )


@then("awssdk no exception should be raised")
def step_no_exception(context: Any) -> None:
    """Check that no exception was raised."""
    assert context.raised_error is None, (  # type: ignore[attr-defined]
        f"Expected no exception, got {context.raised_error_type}: "  # type: ignore[attr-defined]
        f"{context.raised_error}"  # type: ignore[attr-defined]
    )


@then("awssdk no raw credential values should appear in log output")
def step_no_raw_creds_in_log(context: Any) -> None:
    """Check that no raw credential values appear in log output."""
    assert (
        context.bound_resource is not None or context.raised_error is not None  # type: ignore[attr-defined]
    ), "Handler did not run"
    if context.raised_error is not None:  # type: ignore[attr-defined]
        msg = str(context.raised_error)  # type: ignore[attr-defined]
        assert "wJalrXUtnFEMI" not in msg, f"Secret found in error: {msg}"
        assert "AKIAIOSFODNN7EXAMPLE" not in msg, f"Secret found in error: {msg}"
