"""AuthScenario — context manager for structured authorization test scenarios.

Provides a fluent API for defining test subjects, resources, and asserting
authorization outcomes with clear error messages.

Usage:
    with AuthScenario(rp) as s:
        alice = s.subject("alice", roles=["editor"])
        doc = s.resource("doc-1", type="documents", owner_id="alice")
        s.assert_that(alice, "documents:write", doc, must_allow)
"""

from __future__ import annotations

from collections.abc import Callable
from typing import Any

from pyauthz.core.decision import Decision
from pyauthz.core.engine import PyAuthz
from pyauthz.models.resource import Resource
from pyauthz.models.subject import Subject


class AuthScenario:
    """Context manager for authorization test scenarios.

    Tracks all assertions and provides summary reporting. Raises
    AssertionError on failure with a detailed message showing the
    decision's reasoning chain.

    Attributes:
        pyauthz: The PyAuthz engine to test against.
        env: Environment context for all evaluations in this scenario.
    """

    def __init__(self, pyauthz: PyAuthz, **env: Any) -> None:
        self._pyauthz = pyauthz
        self._env = env
        self._results: list[tuple[str, bool, str]] = []

    def __enter__(self) -> AuthScenario:
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        pass

    def subject(
        self,
        id: str,
        *,
        roles: list[str] | None = None,
        **attributes: Any,
    ) -> Subject:
        """Create a Subject for this scenario.

        Args:
            id: Subject identifier.
            roles: List of role names.
            **attributes: Additional attributes.

        Returns:
            A Subject instance.
        """
        return Subject(id=id, roles=roles or [], attributes=attributes)

    def resource(
        self,
        id: str,
        *,
        type: str = "default",
        owner_id: str | None = None,
        **attributes: Any,
    ) -> Resource:
        """Create a Resource for this scenario.

        Args:
            id: Resource identifier.
            type: Resource type string.
            owner_id: Optional owner ID.
            **attributes: Additional attributes.

        Returns:
            A Resource instance.
        """
        return Resource(id=id, type=type, owner_id=owner_id or "", attributes=attributes)

    def evaluate(
        self,
        subject: Subject,
        action: str,
        resource: Resource,
        **env: Any,
    ) -> Decision:
        """Evaluate an authorization request and return the Decision.

        Does not raise Denied — always returns the Decision object.

        Args:
            subject: The entity requesting access.
            action: The action string.
            resource: The resource being accessed.
            **env: Per-evaluation environment overrides.

        Returns:
            The Decision object.
        """
        merged_env = {**self._env, **env}
        return self._pyauthz._evaluate(subject, action, resource, merged_env)

    def assert_that(
        self,
        subject: Subject,
        action: str,
        resource: Resource,
        predicate: Callable[[Decision], tuple[bool, str]],
        **env: Any,
    ) -> Decision:
        """Assert that an authorization decision matches the predicate.

        Evaluates the request and runs the predicate (e.g., must_allow,
        must_deny). Raises AssertionError on failure with a detailed message.

        Args:
            subject: The entity requesting access.
            action: The action string.
            resource: The resource being accessed.
            predicate: A callable that takes a Decision and returns (passed, message).
            **env: Per-evaluation environment overrides.

        Returns:
            The Decision object (for further inspection if needed).

        Raises:
            AssertionError: If the predicate fails.
        """
        decision = self.evaluate(subject, action, resource, **env)
        passed, message = predicate(decision)

        label = f"{subject.id} -> {action} on {resource.type}/{resource.id}"
        self._results.append((label, passed, message))

        if not passed:
            reasoning = "\n  ".join(decision.reasoning) if decision.reasoning else "(no reasoning)"
            matched = (
                ", ".join(decision.matched_policies) if decision.matched_policies else "(none)"
            )
            raise AssertionError(
                f"Authorization assertion failed: {label}\n"
                f"  {message}\n"
                f"  Matched policies: {matched}\n"
                f"  Reasoning:\n  {reasoning}"
            )

        return decision

    def assert_bulk(
        self,
        scenarios: list[tuple[Subject, str, Resource, Callable[[Decision], tuple[bool, str]]]],
        **env: Any,
    ) -> list[Decision]:
        """Assert multiple authorization scenarios at once.

        Unlike assert_that, this collects ALL failures before raising,
        giving a complete picture of what passed and what failed.

        Args:
            scenarios: List of (subject, action, resource, predicate) tuples.
            **env: Shared environment context.

        Returns:
            List of Decision objects.

        Raises:
            AssertionError: If any predicate fails, with all failures listed.
        """
        decisions: list[Decision] = []
        failures: list[str] = []

        for subject, action, resource, predicate in scenarios:
            decision = self.evaluate(subject, action, resource, **env)
            decisions.append(decision)
            passed, message = predicate(decision)
            label = f"{subject.id} -> {action} on {resource.type}/{resource.id}"
            self._results.append((label, passed, message))
            if not passed:
                failures.append(f"  {label}: {message}")

        if failures:
            failure_list = "\n".join(failures)
            raise AssertionError(
                f"{len(failures)} authorization assertion(s) failed:\n{failure_list}"
            )

        return decisions

    @property
    def results(self) -> list[tuple[str, bool, str]]:
        """All recorded assertion results as (label, passed, message) tuples."""
        return list(self._results)
