"""Step definitions for resource_virtual_types.feature.

Tests the VirtualResource base class and example implementations
(MetricResource, APIEndpointResource).
"""

from __future__ import annotations

import ast
from typing import Any

from behave import given, then, when

from cleveragents.resource.virtual import (
    APIEndpointResource,
    MetricResource,
    VirtualResource,
)


def _parse_lambda_body(compute_fn_str: str) -> Any:
    """Parse a simple 'lambda: <expr>' string and return the literal value.

    Only supports ``lambda: <literal>`` forms where the body is a Python
    literal (int, float, str, dict, list, bool, None).  This avoids the
    security risks of ``eval()`` while still supporting the limited set of
    compute-function strings used in Gherkin tables.

    Args:
        compute_fn_str: A string of the form ``"lambda: <literal>"``.

    Returns:
        The parsed literal value.

    Raises:
        ValueError: If the string is not a supported ``lambda: <literal>`` form.
    """
    stripped = compute_fn_str.strip()
    prefix = "lambda:"
    if not stripped.startswith(prefix):
        raise ValueError(
            f"Unsupported compute_fn format: {compute_fn_str!r}. "
            "Only 'lambda: <literal>' forms are supported in Gherkin tables."
        )
    body = stripped[len(prefix) :].strip()
    try:
        return ast.literal_eval(body)
    except (ValueError, SyntaxError) as exc:
        raise ValueError(f"Cannot safely parse lambda body {body!r}: {exc}") from exc


# ────────────────────────────────────────────────────────────
# Background steps
# ────────────────────────────────────────────────────────────


@given("I have imported the virtual resource module")
def step_import_virtual_module(context: object) -> None:
    """Verify the virtual resource module is imported."""
    assert VirtualResource is not None
    assert MetricResource is not None
    assert APIEndpointResource is not None


# ────────────────────────────────────────────────────────────
# VirtualResource creation steps
# ────────────────────────────────────────────────────────────


@when("I create a VirtualResource with:")
def step_create_virtual_resource(context: object) -> None:
    """Create a VirtualResource from table data."""
    table_dict = {row["name"]: row["value"] for row in context.table}

    name = table_dict.get("name", "test-resource")
    description = table_dict.get("description", "Test resource")
    compute_fn_str = table_dict.get("compute_fn", "lambda: 42")

    literal_value = _parse_lambda_body(compute_fn_str)
    compute_fn = lambda: literal_value  # noqa: E731

    resource = VirtualResource(
        name=name,
        description=description,
        compute_fn=compute_fn,
    )

    context.virtual_resource = resource


@then("the VirtualResource should have:")
def step_check_virtual_resource_attrs(context: object) -> None:
    """Check VirtualResource attributes."""
    resource = context.virtual_resource

    for row in context.table:
        attr_name = row["name"]
        expected_value = row["value"]

        actual_value = getattr(resource, attr_name)
        assert actual_value == expected_value, (
            f"Expected {attr_name}={expected_value!r}, got {actual_value!r}"
        )


@then("the VirtualResource should be callable")
def step_virtual_resource_callable(context: object) -> None:
    """Check that VirtualResource has a compute method."""
    resource = context.virtual_resource
    assert hasattr(resource, "compute"), "VirtualResource should have compute method"
    assert callable(resource.compute), "compute should be callable"


# ────────────────────────────────────────────────────────────
# VirtualResource computation steps
# ────────────────────────────────────────────────────────────


@given('I have a VirtualResource named "{name}" with compute_fn returning {value}')
def step_create_virtual_with_value(context: object, name: str, value: str) -> None:
    """Create a VirtualResource that returns a specific value."""
    if value.lower() == "true":
        computed_value: Any = True
    elif value.lower() == "false":
        computed_value = False
    elif value.isdigit():
        computed_value = int(value)
    else:
        try:
            computed_value = float(value)
        except ValueError:
            computed_value = value

    resource = VirtualResource(
        name=name,
        description=f"Test resource {name}",
        compute_fn=lambda: computed_value,
    )
    context.virtual_resource = resource


@when("I compute the virtual resource")
def step_compute_virtual_resource(context: object) -> None:
    """Compute the virtual resource value.

    Exceptions raised by the compute function are captured into
    ``context.computation_error`` so that downstream ``Then`` steps can
    assert on them; without this, an expected-exception scenario would
    error at the ``When`` step before reaching the ``Then``.
    """
    resource = context.virtual_resource
    try:
        context.computed_value = resource.compute()
        context.computation_error = None
    except Exception as exc:
        context.computed_value = None
        context.computation_error = exc


@then("the computed value should be {expected}")
def step_check_computed_value(context: object, expected: str) -> None:
    """Check the computed value."""
    if expected.lower() == "true":
        expected_value: Any = True
    elif expected.lower() == "false":
        expected_value = False
    elif expected.isdigit():
        expected_value = int(expected)
    else:
        try:
            expected_value = float(expected)
        except ValueError:
            expected_value = expected

    actual = context.computed_value
    assert actual == expected_value, f"Expected {expected_value!r}, got {actual!r}"


# ────────────────────────────────────────────────────────────
# Error handling steps
# ────────────────────────────────────────────────────────────


@when('I try to create a VirtualResource with invalid name "{name}"')
def step_create_with_invalid_name(context: object, name: str) -> None:
    """Try to create a VirtualResource with invalid name."""
    try:
        VirtualResource(
            name=name,
            description="Test",
            compute_fn=lambda: 42,
        )
        context.creation_error = None
    except (ValueError, TypeError) as exc:
        context.creation_error = exc


@when("I try to create a VirtualResource with an empty name")
def step_create_with_empty_name(context: object) -> None:
    """Try to create a VirtualResource with an empty name."""
    try:
        VirtualResource(
            name="",
            description="Test",
            compute_fn=lambda: 42,
        )
        context.creation_error = None
    except (ValueError, TypeError) as exc:
        context.creation_error = exc


@when('I try to create a VirtualResource with non-callable compute_fn "{fn}"')
def step_create_with_non_callable(context: object, fn: str) -> None:
    """Try to create a VirtualResource with non-callable compute_fn."""
    non_callable: Any = fn
    try:
        VirtualResource(
            name="test",
            description="Test",
            compute_fn=non_callable,
        )
        context.creation_error = None
    except (ValueError, TypeError) as exc:
        context.creation_error = exc


@then(
    'the virtual resource creation should raise {error_type} with message containing "{message}"'
)
def step_check_error(context: object, error_type: str, message: str) -> None:
    """Check that the expected error was raised."""
    error = context.creation_error
    assert error is not None, f"Expected {error_type} but no error was raised"

    error_class_name = error.__class__.__name__
    assert error_class_name == error_type, (
        f"Expected {error_type}, got {error_class_name}"
    )

    assert message.lower() in str(error).lower(), (
        f"Error message does not contain '{message}': {error}"
    )


# ────────────────────────────────────────────────────────────
# MetricResource steps
# ────────────────────────────────────────────────────────────


@when("I create a MetricResource with:")
def step_create_metric_resource(context: object) -> None:
    """Create a MetricResource from table data."""
    table_dict = {row["name"]: row["value"] for row in context.table}

    name = table_dict.get("name", "test-metric")
    description = table_dict.get("description", "Test metric")
    compute_fn_str = table_dict.get("compute_fn", "lambda: 42.0")
    unit = table_dict.get("unit", "")

    literal_value = _parse_lambda_body(compute_fn_str)
    compute_fn = lambda: literal_value  # noqa: E731

    resource = MetricResource(
        name=name,
        description=description,
        compute_fn=compute_fn,
        unit=unit,
    )

    context.metric_resource = resource


@then("the MetricResource should have:")
def step_check_metric_attrs(context: object) -> None:
    """Check MetricResource attributes."""
    resource = context.metric_resource

    for row in context.table:
        attr_name = row["name"]
        expected_value = row["value"]

        actual_value = getattr(resource, attr_name)
        assert actual_value == expected_value, (
            f"Expected {attr_name}={expected_value!r}, got {actual_value!r}"
        )


@given(
    'I have a MetricResource named "{name}" with compute_fn returning {value} and unit "{unit}"'
)
def step_create_metric_with_value(
    context: object, name: str, value: str, unit: str
) -> None:
    """Create a MetricResource that returns a specific value."""
    computed_value = float(value)

    resource = MetricResource(
        name=name,
        description=f"Test metric {name}",
        compute_fn=lambda: computed_value,
        unit=unit,
    )
    context.metric_resource = resource


@when("I compute the metric resource")
def step_compute_metric_resource(context: object) -> None:
    """Compute the metric resource value."""
    resource = context.metric_resource
    context.computed_value = resource.compute()


# ────────────────────────────────────────────────────────────
# APIEndpointResource steps
# ────────────────────────────────────────────────────────────


@when("I create an APIEndpointResource with:")
def step_create_api_endpoint_resource(context: object) -> None:
    """Create an APIEndpointResource from table data."""
    table_dict = {row["name"]: row["value"] for row in context.table}

    name = table_dict.get("name", "test-api")
    description = table_dict.get("description", "Test API")
    compute_fn_str = table_dict.get("compute_fn", "lambda: {}")
    endpoint_url = table_dict.get("endpoint_url", "")

    literal_value = _parse_lambda_body(compute_fn_str)
    compute_fn = lambda: literal_value  # noqa: E731

    resource = APIEndpointResource(
        name=name,
        description=description,
        compute_fn=compute_fn,
        endpoint_url=endpoint_url,
    )

    context.api_resource = resource


@then("the APIEndpointResource should have:")
def step_check_api_attrs(context: object) -> None:
    """Check APIEndpointResource attributes."""
    resource = context.api_resource

    for row in context.table:
        attr_name = row["name"]
        expected_value = row["value"]

        actual_value = getattr(resource, attr_name)
        assert actual_value == expected_value, (
            f"Expected {attr_name}={expected_value!r}, got {actual_value!r}"
        )


@given('I have an APIEndpointResource named "{name}" with compute_fn returning {value}')
def step_create_api_with_value(context: object, name: str, value: str) -> None:
    """Create an APIEndpointResource that returns a specific value."""
    computed_value: Any = ast.literal_eval(value)

    resource = APIEndpointResource(
        name=name,
        description=f"Test API {name}",
        compute_fn=lambda: computed_value,
    )
    context.api_resource = resource


@when("I compute the API endpoint resource")
def step_compute_api_resource(context: object) -> None:
    """Compute the API endpoint resource value."""
    resource = context.api_resource
    context.computed_value = resource.compute()


@then('the API endpoint computed value is a dict with keys "{keys}"')
def step_api_check_dict_keys(context: object, keys: str) -> None:
    """Check that computed value is a dict with expected keys."""
    value = context.computed_value
    assert isinstance(value, dict), f"Expected dict, got {type(value).__name__}"

    expected_keys = [k.strip() for k in keys.strip('"').split(",")]

    for key in expected_keys:
        assert key in value, f"Expected key '{key}' not in dict: {value.keys()}"


@then('the API endpoint value["{key}"] is "{expected}"')
def step_api_check_dict_value(context: object, key: str, expected: str) -> None:
    """Check a specific value in the computed dict."""
    value = context.computed_value
    assert isinstance(value, dict), f"Expected dict, got {type(value).__name__}"

    if expected.lower() == "true":
        expected_value: Any = True
    elif expected.lower() == "false":
        expected_value = False
    elif expected.isdigit():
        expected_value = int(expected)
    else:
        try:
            expected_value = float(expected)
        except ValueError:
            expected_value = expected

    actual = value.get(key)
    assert actual == expected_value, (
        f"Expected {key}={expected_value!r}, got {actual!r}"
    )


# ────────────────────────────────────────────────────────────
# Kwargs handling steps
# ────────────────────────────────────────────────────────────


@given("I have a VirtualResource with compute_fn that accepts kwargs")
def step_create_virtual_with_kwargs(context: object) -> None:
    """Create a VirtualResource with a compute_fn that accepts kwargs."""

    def compute_with_kwargs(multiplier: int = 1) -> int:
        return 42 * multiplier

    resource = VirtualResource(
        name="test-kwargs",
        description="Test resource with kwargs",
        compute_fn=compute_with_kwargs,
    )
    context.virtual_resource = resource


@when("I compute the virtual resource with kwargs {kwargs_str}")
def step_compute_with_kwargs(context: object, kwargs_str: str) -> None:
    """Compute the virtual resource with kwargs."""
    kwargs: dict[str, Any] = ast.literal_eval(kwargs_str)
    resource = context.virtual_resource
    context.computed_value = resource.compute(**kwargs)


@then("the computed value should reflect the passed kwargs")
def step_check_kwargs_reflected(context: object) -> None:
    """Check that kwargs were properly used."""
    assert context.computed_value == 84, f"Expected 84, got {context.computed_value}"


# ────────────────────────────────────────────────────────────
# String representation steps
# ────────────────────────────────────────────────────────────


@given('I have a VirtualResource named "{name}" with description "{description}"')
def step_create_virtual_for_repr(context: object, name: str, description: str) -> None:
    """Create a VirtualResource for string representation testing."""
    resource = VirtualResource(
        name=name,
        description=description,
        compute_fn=lambda: 42,
    )
    context.virtual_resource = resource


@when("I get the string representation of the virtual resource")
def step_get_string_repr(context: object) -> None:
    """Get the string representation of the resource."""
    resource = (
        getattr(context, "virtual_resource", None)
        or getattr(context, "metric_resource", None)
        or getattr(context, "api_resource", None)
    )
    context.string_repr = repr(resource)


@then('the string representation should contain "{text}"')
def step_check_repr_contains(context: object, text: str) -> None:
    """Check that string representation contains text."""
    assert text in context.string_repr, (
        f"Expected '{text}' in repr: {context.string_repr}"
    )


@given('I have a MetricResource named "{name}" with unit "{unit}"')
def step_create_metric_for_repr(context: object, name: str, unit: str) -> None:
    """Create a MetricResource for string representation testing."""
    resource = MetricResource(
        name=name,
        description="Test metric",
        compute_fn=lambda: 42.0,
        unit=unit,
    )
    context.metric_resource = resource


@given('I have an APIEndpointResource named "{name}" with endpoint_url "{url}"')
def step_create_api_for_repr(context: object, name: str, url: str) -> None:
    """Create an APIEndpointResource for string representation testing."""
    resource = APIEndpointResource(
        name=name,
        description="Test API",
        compute_fn=lambda: {},
        endpoint_url=url,
    )
    context.api_resource = resource


# ────────────────────────────────────────────────────────────
# Complex compute function steps
# ────────────────────────────────────────────────────────────


@given("I have a VirtualResource with a complex compute function")
def step_create_complex_virtual(context: object) -> None:
    """Create a VirtualResource with a complex compute function."""

    def complex_compute() -> dict[str, Any]:
        return {
            "result": 42,
            "nested": {"value": "test"},
            "list": [1, 2, 3],
        }

    resource = VirtualResource(
        name="complex-resource",
        description="Complex resource",
        compute_fn=complex_compute,
    )
    context.virtual_resource = resource


@then("the complex computed value should be correct")
def step_check_complex_value(context: object) -> None:
    """Check that complex computed value is correct."""
    value = context.computed_value
    assert isinstance(value, dict), f"Expected dict, got {type(value).__name__}"
    assert value["result"] == 42
    assert value["nested"]["value"] == "test"
    assert value["list"] == [1, 2, 3]


# ────────────────────────────────────────────────────────────
# Multiple resources steps
# ────────────────────────────────────────────────────────────


@given("I have created {count:d} different virtual resources")
def step_create_multiple_resources(context: object, count: int) -> None:
    """Create multiple different virtual resources."""
    resources = []
    for i in range(count):
        resource = VirtualResource(
            name=f"resource-{i}",
            description=f"Resource {i}",
            compute_fn=lambda i=i: i * 10,
        )
        resources.append(resource)

    context.resources = resources


@when("I compute all of them")
def step_compute_all_resources(context: object) -> None:
    """Compute all resources."""
    context.computed_values = [r.compute() for r in context.resources]


@then("each should return its expected value")
def step_check_each_value(context: object) -> None:
    """Check that each resource returned its expected value."""
    for i, value in enumerate(context.computed_values):
        expected = i * 10
        assert value == expected, f"Resource {i}: expected {expected}, got {value}"


@then("they should not interfere with each other")
def step_check_no_interference(context: object) -> None:
    """Check that resources don't interfere with each other."""
    assert len(context.computed_values) == len(context.resources)


# ────────────────────────────────────────────────────────────
# Exception handling steps
# ────────────────────────────────────────────────────────────


@given("I have a VirtualResource with compute_fn that raises an exception")
def step_create_virtual_with_exception(context: object) -> None:
    """Create a VirtualResource with compute_fn that raises an exception."""

    def compute_with_error() -> None:
        raise RuntimeError("Test error")

    resource = VirtualResource(
        name="error-resource",
        description="Resource that raises",
        compute_fn=compute_with_error,
    )
    context.virtual_resource = resource


@then("it should raise the exception from compute_fn")
def step_check_exception_raised(context: object) -> None:
    """Check that the exception was raised."""
    try:
        context.virtual_resource.compute()
        raise AssertionError("Expected RuntimeError but no exception was raised")
    except RuntimeError as exc:
        assert "Test error" in str(exc)


# ────────────────────────────────────────────────────────────
# Default kwargs steps
# ────────────────────────────────────────────────────────────


@given("I have a VirtualResource with compute_fn that has default kwargs")
def step_create_virtual_with_defaults(context: object) -> None:
    """Create a VirtualResource with default kwargs."""

    def compute_with_defaults(value: int = 100) -> int:
        return value

    resource = VirtualResource(
        name="default-resource",
        description="Resource with defaults",
        compute_fn=compute_with_defaults,
    )
    context.virtual_resource = resource


@when("I compute without providing kwargs")
def step_compute_without_kwargs(context: object) -> None:
    """Compute without providing kwargs."""
    context.computed_value = context.virtual_resource.compute()


@then("it should use the default values")
def step_check_defaults_used(context: object) -> None:
    """Check that default values were used."""
    assert context.computed_value == 100, (
        f"Expected 100 (default), got {context.computed_value}"
    )


# ────────────────────────────────────────────────────────────
# Inheritance steps
# ────────────────────────────────────────────────────────────


@given("I have created a custom VirtualResource subclass")
def step_create_custom_subclass(context: object) -> None:
    """Create a custom VirtualResource subclass."""

    class CustomResource(VirtualResource[str]):
        """Custom virtual resource subclass."""

        def __init__(self, name: str, description: str) -> None:
            super().__init__(
                name=name,
                description=description,
                compute_fn=self._compute,
            )

        def _compute(self) -> str:
            return f"Custom: {self.name}"

    resource = CustomResource(
        name="custom-resource",
        description="Custom implementation",
    )
    context.custom_resource = resource


@when("I instantiate and compute it")
def step_compute_custom(context: object) -> None:
    """Compute the custom resource."""
    context.computed_value = context.custom_resource.compute()


@then("it should work correctly with the custom implementation")
def step_check_custom_works(context: object) -> None:
    """Check that custom implementation works."""
    expected = "Custom: custom-resource"
    assert context.computed_value == expected, (
        f"Expected '{expected}', got '{context.computed_value}'"
    )
