# Copyright 2025 Tenro.ai
# SPDX-License-Identifier: Apache-2.0

"""Output verification utilities.

Unified helpers for verifying output values:
- `output`: Smart match (dict=subset, scalar=exact)
- `output_contains`: Substring match (strings only)
- `output_exact`: Strict deep equality
"""

from __future__ import annotations

from collections.abc import Callable
from typing import Any

from tenro.util.list_helpers import normalize_and_validate_index


def is_subset_match(actual: Any, expected: Any) -> bool:
    """Check if actual contains expected (deep subset for dicts, exact for scalars)."""
    if isinstance(expected, dict) and isinstance(actual, dict):
        return all(k in actual and is_subset_match(actual[k], v) for k, v in expected.items())
    return bool(actual == expected)


def _verify_output(
    items: list[Any],
    content_field: str,
    matcher: Callable[[Any], bool],
    description: str,
    item_type: str,
    *,
    call_index: int | None = 0,
) -> None:
    """Verify output using a custom matcher function.

    Args:
        `items`: List of items to check.
        `content_field`: Attribute name to read from each item.
        `matcher`: Predicate returning `True` on match.
        `description`: Description for error messages.
        `item_type`: Label for items (e.g., "tool", "agent").
        `call_index`: Index to check (`None` checks all items).

    Raises:
        AssertionError: If no items exist or none match.
    """
    if not items:
        raise AssertionError(f"Expected {item_type} {description}, but none found.")

    if call_index is None:
        # Check all items
        for item in items:
            if matcher(getattr(item, content_field, None)):
                return
        raise AssertionError(f"Expected {item_type} {description}, but none matched.")

    # Check specific index
    item, _ = normalize_and_validate_index(items, call_index, item_type)
    actual = getattr(item, content_field, None)
    if matcher(actual):
        return
    raise AssertionError(f"Expected {item_type}[{call_index}] {description}, got: {actual!r}")


def verify_output(
    items: list[Any],
    content_field: str,
    expected: Any,
    item_type: str,
    *,
    call_index: int | None = 0,
) -> None:
    """Verify output matches expected (dict=subset, scalar=exact)."""
    _verify_output(
        items,
        content_field,
        lambda actual: is_subset_match(actual, expected),
        f"matching {expected!r}",
        item_type,
        call_index=call_index,
    )


def verify_output_exact(
    items: list[Any],
    content_field: str,
    expected: Any,
    item_type: str,
    *,
    call_index: int | None = 0,
) -> None:
    """Verify output using strict deep equality."""
    _verify_output(
        items,
        content_field,
        lambda actual: actual == expected,
        f"equal to {expected!r}",
        item_type,
        call_index=call_index,
    )


def verify_output_contains(
    items: list[Any],
    content_field: str,
    expected_text: str,
    item_type: str,
    *,
    call_index: int | None = 0,
) -> None:
    """Verify output contains expected substring (strings only)."""
    _verify_output(
        items,
        content_field,
        lambda actual: isinstance(actual, str) and expected_text in actual,
        f"containing '{expected_text}'",
        item_type,
        call_index=call_index,
    )


__all__ = [
    "is_subset_match",
    "verify_output",
    "verify_output_exact",
    "verify_output_contains",
]
