from types import TracebackType
from typing import Any, Self, TypeIs, cast

from snektest.models import AssertionFailure, BadRequestError


def assert_raises[T](
    *expected_exceptions: type[T],
    msg: str | None = None,
) -> RaisesContex[T]:
    return RaisesContex(*expected_exceptions, msg=msg)


class RaisesContex[T]:
    def __init__(self, *expected_exceptions: type[T], msg: str | None = None) -> None:
        self.msg = msg
        self.expected_exceptions = expected_exceptions
        self._exception: T | None = None
        self.has_exited = False

    def __enter__(self) -> Self:
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_value: BaseException | None,
        traceback: TracebackType | None,
    ) -> bool:
        self.has_exited = True
        if exc_type is None:
            message = (
                self.msg or "Expected to raise an exception but no exception was raised"
            )
            raise AssertionFailure(
                message,
                actual=None,
                expected=self.expected_exceptions,
            )
        if not isinstance(exc_value, self.expected_exceptions):
            expected_exceptions_display_name = " | ".join(
                e.__name__ for e in self.expected_exceptions
            )
            message = (
                self.msg
                or f"Expected to raise {expected_exceptions_display_name} but raised {type(exc_value).__name__}"
            )
            raise AssertionFailure(
                message,
                actual=exc_value,
                expected=self.expected_exceptions,
            )
        self._exception = exc_value
        return True

    @property
    def exception(self) -> T:
        if not self.has_exited:
            msg = "Exception under assert_raises was accessed before exiting the context manager"
            raise BadRequestError(msg)
        return cast("T", self._exception)


def assert_eq(actual: Any, expected: Any, *, msg: str | None = None) -> None:
    """Assert that actual == expected."""
    if actual != expected:
        message = msg or f"{actual!r} != {expected!r}"
        raise AssertionFailure(
            message,
            actual=actual,
            expected=expected,
            operator="==",
        )


def assert_ne(actual: Any, expected: Any, *, msg: str | None = None) -> None:
    """Assert that actual != expected."""
    if actual == expected:
        message = msg or f"{actual!r} == {expected!r}"
        raise AssertionFailure(
            message,
            actual=actual,
            expected=expected,
            operator="!=",
        )


def assert_true(value: Any, *, msg: str | None = None) -> None:
    """Assert that value is True (identity check, not truthiness)."""
    if value is not True:
        message = msg or f"{value!r} is not True"
        raise AssertionFailure(
            message,
            actual=value,
            expected=True,
            operator="is",
        )


def assert_false(value: Any, *, msg: str | None = None) -> None:
    """Assert that value is False (identity check, not falsiness)."""
    if value is not False:
        message = msg or f"{value!r} is not False"
        raise AssertionFailure(
            message,
            actual=value,
            expected=False,
            operator="is",
        )


def assert_is_none(value: Any, *, msg: str | None = None) -> None:
    """Assert that value is None."""
    if value is not None:
        message = msg or f"{value!r} is not None"
        raise AssertionFailure(
            message,
            actual=value,
            expected=None,
            operator="is",
        )


def assert_is_not_none[T](value: T | None, *, msg: str | None = None) -> TypeIs[T]:
    """Assert that value is not None."""
    if value is None:
        message = msg or "value is None"
        raise AssertionFailure(
            message,
            actual=value,
            expected="not None",
            operator="is not",
        )
    return True


def assert_is(actual: Any, expected: Any, *, msg: str | None = None) -> None:
    """Assert that actual is expected (identity check)."""
    if actual is not expected:
        message = msg or f"{actual!r} is not {expected!r}"
        raise AssertionFailure(
            message,
            actual=actual,
            expected=expected,
            operator="is",
        )


def assert_is_not(actual: Any, expected: Any, *, msg: str | None = None) -> None:
    """Assert that actual is not expected (identity check)."""
    if actual is expected:
        message = msg or f"{actual!r} is {expected!r}"
        raise AssertionFailure(
            message,
            actual=actual,
            expected=expected,
            operator="is not",
        )


def assert_lt(actual: Any, expected: Any, *, msg: str | None = None) -> None:
    """Assert that actual < expected."""
    if not actual < expected:
        message = msg or f"{actual!r} >= {expected!r}"
        raise AssertionFailure(
            message,
            actual=actual,
            expected=expected,
            operator="<",
        )


def assert_gt(actual: Any, expected: Any, *, msg: str | None = None) -> None:
    """Assert that actual > expected."""
    if not actual > expected:
        message = msg or f"{actual!r} <= {expected!r}"
        raise AssertionFailure(
            message,
            actual=actual,
            expected=expected,
            operator=">",
        )


def assert_le(actual: Any, expected: Any, *, msg: str | None = None) -> None:
    """Assert that actual <= expected."""
    if not actual <= expected:
        message = msg or f"{actual!r} > {expected!r}"
        raise AssertionFailure(
            message,
            actual=actual,
            expected=expected,
            operator="<=",
        )


def assert_ge(actual: Any, expected: Any, *, msg: str | None = None) -> None:
    """Assert that actual >= expected."""
    if not actual >= expected:
        message = msg or f"{actual!r} < {expected!r}"
        raise AssertionFailure(
            message,
            actual=actual,
            expected=expected,
            operator=">=",
        )


def assert_in(member: Any, container: Any, *, msg: str | None = None) -> None:
    """Assert that member in container."""
    if member not in container:
        message = msg or f"{member!r} not found in {container!r}"
        raise AssertionFailure(
            message,
            actual=member,
            expected=container,
            operator="in",
        )


def assert_not_in(member: Any, container: Any, *, msg: str | None = None) -> None:
    """Assert that member not in container."""
    if member in container:
        message = msg or f"{member!r} found in {container!r}"
        raise AssertionFailure(
            message,
            actual=member,
            expected=container,
            operator="not in",
        )


def assert_isinstance(
    obj: Any, classinfo: type | tuple[type, ...], *, msg: str | None = None
) -> None:
    """Assert that isinstance(obj, classinfo) is True."""
    if not isinstance(obj, classinfo):
        type_name = (
            classinfo.__name__ if isinstance(classinfo, type) else str(classinfo)
        )
        message = msg or f"{obj!r} is not an instance of {type_name}"
        raise AssertionFailure(
            message,
            actual=type(obj).__name__,
            expected=type_name,
            operator="isinstance",
        )


def assert_not_isinstance(
    obj: Any, classinfo: type | tuple[type, ...], *, msg: str | None = None
) -> None:
    """Assert that isinstance(obj, classinfo) is False."""
    if isinstance(obj, classinfo):
        type_name = (
            classinfo.__name__ if isinstance(classinfo, type) else str(classinfo)
        )
        message = msg or f"{obj!r} is an instance of {type_name}"
        raise AssertionFailure(
            message,
            actual=type(obj).__name__,
            expected=f"not {type_name}",
            operator="not isinstance",
        )


def assert_len(obj: Any, expected_length: int, *, msg: str | None = None) -> None:
    """Assert that len(obj) == expected_length."""
    actual_length = len(obj)
    if actual_length != expected_length:
        message = msg or f"Length {actual_length} != {expected_length}"
        raise AssertionFailure(
            message,
            actual=actual_length,
            expected=expected_length,
            operator="len ==",
        )


def fail(msg: str | None = None) -> None:
    """Unconditionally raise an AssertionFailure.

    Args:
        msg: Optional custom message
    """
    message = msg or "Assertion failed"
    raise AssertionFailure(message)
