import pytest
from django.conf import settings
from djmoney.money import Money

from order.exceptions import (
    InsufficientStockError,
    InvalidOrderDataError,
    InvalidStatusTransitionError,
    OrderCancellationError,
    OrderNotFoundError,
    OrderServiceError,
    PaymentError,
    PaymentNotFoundError,
    PaymentVerificationError,
    ProductNotFoundError,
    StockError,
    StockReservationError,
    WebhookVerificationError,
)
from order.factories.order import OrderFactory
from order.models.order import OrderStatus
from order.services import OrderService
from order.stock import StockManager
from product.factories.product import ProductFactory


@pytest.mark.django_db
class TestCorrectExceptionTypesAreRaised:
    """
    Test that appropriate custom exceptions are raised for each error condition
    throughout the order and stock management system.
    """

    # ========================================================================
    # Stock-Related Exception Tests
    # ========================================================================

    @pytest.mark.parametrize(
        "stock,quantity,expected_exception,description",
        [
            # InsufficientStockError scenarios
            (5, 10, InsufficientStockError, "quantity_exceeds_stock"),
            (0, 1, InsufficientStockError, "zero_stock"),
            (3, 5, InsufficientStockError, "quantity_slightly_over"),
            (10, 11, InsufficientStockError, "one_over_stock"),
            (100, 150, InsufficientStockError, "large_quantity_exceeds"),
        ],
        ids=[
            "quantity_exceeds_stock",
            "zero_stock",
            "quantity_slightly_over",
            "one_over_stock",
            "large_quantity_exceeds",
        ],
    )
    def test_insufficient_stock_error_raised_for_stock_issues(
        self, stock, quantity, expected_exception, description
    ):
        """
        Test that InsufficientStockError is raised when stock is insufficient.

        Verifies that stock operations raise InsufficientStockError with
        correct context when requested quantity exceeds available stock.
        """
        # Create product with specified stock
        product = ProductFactory.create(
            price=Money("50.00", settings.DEFAULT_CURRENCY), stock=stock
        )
        product.set_current_language("en")
        product.name = f"Test Product - {description}"
        product.save()

        # Attempt to reserve stock with insufficient quantity
        with pytest.raises(expected_exception) as exc_info:
            StockManager.reserve_stock(
                product_id=product.id,
                quantity=quantity,
                session_id="test-session-123",
                user_id=None,
            )

        # Verify exception attributes
        exception = exc_info.value
        assert exception.product_id == product.id, (
            "Exception should contain correct product_id"
        )
        assert exception.available == stock, (
            f"Exception should show available stock as {stock}"
        )
        assert exception.requested == quantity, (
            f"Exception should show requested quantity as {quantity}"
        )

        # Verify exception message is informative
        error_message = str(exception)
        assert str(product.id) in error_message, (
            "Error message should contain product ID"
        )
        assert str(stock) in error_message, (
            "Error message should contain available stock"
        )
        assert str(quantity) in error_message, (
            "Error message should contain requested quantity"
        )

    @pytest.mark.parametrize(
        "operation,product_exists,expected_exception",
        [
            ("reserve_stock", False, ProductNotFoundError),
            ("decrement_stock", False, ProductNotFoundError),
            ("increment_stock", False, ProductNotFoundError),
            ("get_available_stock", False, ProductNotFoundError),
        ],
        ids=[
            "reserve_nonexistent_product",
            "decrement_nonexistent_product",
            "increment_nonexistent_product",
            "get_available_nonexistent_product",
        ],
    )
    def test_product_not_found_error_raised_for_missing_products(
        self, operation, product_exists, expected_exception
    ):
        """
        Test that ProductNotFoundError is raised for missing products.

        Verifies that stock operations raise ProductNotFoundError when
        attempting to operate on non-existent products.
        """
        # Use a product ID that doesn't exist
        nonexistent_product_id = 999999

        # Verify product doesn't exist
        from product.models import Product

        assert not Product.objects.filter(id=nonexistent_product_id).exists()

        # Attempt operation on non-existent product
        with pytest.raises(expected_exception) as exc_info:
            if operation == "reserve_stock":
                StockManager.reserve_stock(
                    product_id=nonexistent_product_id,
                    quantity=1,
                    session_id="test-session",
                    user_id=None,
                )
            elif operation == "decrement_stock":
                StockManager.decrement_stock(
                    product_id=nonexistent_product_id,
                    quantity=1,
                    order_id=1,
                    reason="test",
                )
            elif operation == "increment_stock":
                StockManager.increment_stock(
                    product_id=nonexistent_product_id,
                    quantity=1,
                    order_id=1,
                    reason="test",
                )
            elif operation == "get_available_stock":
                StockManager.get_available_stock(
                    product_id=nonexistent_product_id
                )

        # Verify exception attributes
        exception = exc_info.value
        assert exception.product_id == nonexistent_product_id, (
            "Exception should contain correct product_id"
        )

        # Verify exception message
        error_message = str(exception)
        assert str(nonexistent_product_id) in error_message, (
            "Error message should contain product ID"
        )
        assert "not found" in error_message.lower(), (
            "Error message should indicate product not found"
        )

    # ========================================================================
    # Order Status Transition Exception Tests
    # ========================================================================

    @pytest.mark.parametrize(
        "current_status,invalid_new_status,description",
        [
            # Backwards transitions (not allowed)
            (
                OrderStatus.SHIPPED,
                OrderStatus.PROCESSING,
                "shipped_to_processing",
            ),
            (
                OrderStatus.DELIVERED,
                OrderStatus.SHIPPED,
                "delivered_to_shipped",
            ),
            (
                OrderStatus.PROCESSING,
                OrderStatus.PENDING,
                "processing_to_pending",
            ),
            (
                OrderStatus.COMPLETED,
                OrderStatus.DELIVERED,
                "completed_to_delivered",
            ),
            # Invalid forward transitions
            (
                OrderStatus.PENDING,
                OrderStatus.SHIPPED,
                "pending_to_shipped_skip",
            ),
            (
                OrderStatus.PROCESSING,
                OrderStatus.DELIVERED,
                "processing_to_delivered_skip",
            ),
            (
                OrderStatus.SHIPPED,
                OrderStatus.COMPLETED,
                "shipped_to_completed_skip",
            ),
            # Invalid transitions from terminal states
            (
                OrderStatus.CANCELED,
                OrderStatus.PROCESSING,
                "canceled_to_processing",
            ),
            (
                OrderStatus.REFUNDED,
                OrderStatus.PROCESSING,
                "refunded_to_processing",
            ),
        ],
        ids=[
            "shipped_to_processing",
            "delivered_to_shipped",
            "processing_to_pending",
            "completed_to_delivered",
            "pending_to_shipped_skip",
            "processing_to_delivered_skip",
            "shipped_to_completed_skip",
            "canceled_to_processing",
            "refunded_to_processing",
        ],
    )
    def test_invalid_status_transition_error_raised_for_invalid_transitions(
        self, current_status, invalid_new_status, description
    ):
        """
        Test that InvalidStatusTransitionError is raised for invalid transitions.

        Verifies that order status transitions raise InvalidStatusTransitionError
        when attempting invalid state changes.
        """
        # Create order with current status
        order = OrderFactory.create(status=current_status)

        # Attempt invalid status transition
        with pytest.raises(InvalidStatusTransitionError) as exc_info:
            OrderService.update_order_status(order, invalid_new_status)

        # Verify exception attributes
        exception = exc_info.value
        assert exception.current_status == current_status, (
            f"Exception should show current status as {current_status}"
        )
        assert exception.new_status == invalid_new_status, (
            f"Exception should show attempted new status as {invalid_new_status}"
        )
        assert isinstance(exception.allowed, list), (
            "Exception should contain list of allowed transitions"
        )

        # Verify exception message is informative
        error_message = str(exception)
        assert current_status in error_message, (
            "Error message should contain current status"
        )
        assert invalid_new_status in error_message, (
            "Error message should contain attempted new status"
        )
        assert (
            "allowed" in error_message.lower()
            or "transition" in error_message.lower()
        ), "Error message should mention allowed transitions"

    # ========================================================================
    # Order Data Validation Exception Tests
    # ========================================================================

    @pytest.mark.parametrize(
        "invalid_data_scenario,expected_exception,description",
        [
            ("invalid_quantity_zero", InvalidOrderDataError, "quantity_zero"),
            (
                "invalid_quantity_negative",
                InvalidOrderDataError,
                "quantity_negative",
            ),
            ("missing_product", InvalidOrderDataError, "product_required"),
        ],
        ids=[
            "quantity_zero",
            "quantity_negative",
            "product_required",
        ],
    )
    def test_invalid_order_data_error_raised_for_validation_failures(
        self, invalid_data_scenario, expected_exception, description
    ):
        """
        Test that InvalidOrderDataError is raised for order validation failures.

        Verifies that order creation raises InvalidOrderDataError when
        order data fails validation.
        """
        # Create a valid product for testing
        product = ProductFactory.create(
            price=Money("50.00", settings.DEFAULT_CURRENCY), stock=10
        )
        product.set_current_language("en")
        product.name = "Test Product"
        product.save()

        # Prepare order and items data based on scenario
        order_data = {
            "shipping_price": Money("10.00", settings.DEFAULT_CURRENCY),
        }

        if invalid_data_scenario == "invalid_quantity_zero":
            items_data = [
                {
                    "product": product,
                    "quantity": 0,  # Invalid: zero quantity
                }
            ]
        elif invalid_data_scenario == "invalid_quantity_negative":
            items_data = [
                {
                    "product": product,
                    "quantity": -5,  # Invalid: negative quantity
                }
            ]
        elif invalid_data_scenario == "missing_product":
            items_data = [
                {
                    "product": None,  # Invalid: missing product
                    "quantity": 1,
                }
            ]

        # Attempt to create order with invalid data
        with pytest.raises(expected_exception) as exc_info:
            OrderService.create_order(
                order_data=order_data, items_data=items_data, user=None
            )

        # Verify exception is raised
        exception = exc_info.value
        assert isinstance(exception, expected_exception), (
            f"Should raise {expected_exception.__name__}"
        )

        # Verify exception message is informative
        error_message = str(exception)
        assert len(error_message) > 0, "Error message should not be empty"

    # ========================================================================
    # Payment Exception Tests
    # ========================================================================

    @pytest.mark.parametrize(
        "payment_scenario,expected_exception,description",
        [
            ("missing_payment_id", PaymentNotFoundError, "no_payment_intent"),
        ],
        ids=[
            "no_payment_intent",
        ],
    )
    def test_payment_not_found_error_raised_for_payment_issues(
        self, payment_scenario, expected_exception, description
    ):
        """
        Test that PaymentNotFoundError is raised for payment-related issues.

        Verifies that payment operations raise PaymentNotFoundError when
        payment intent is missing or invalid.
        """
        # Create a valid product and cart for testing
        product = ProductFactory.create(
            price=Money("50.00", settings.DEFAULT_CURRENCY), stock=10
        )
        product.set_current_language("en")
        product.name = "Test Product"
        product.save()

        # Create a cart with items
        from cart.factories.cart import CartFactory
        from cart.factories.item import CartItemFactory

        cart = CartFactory.create()
        CartItemFactory.create(cart=cart, product=product, quantity=1)

        # Prepare payment data based on scenario
        if payment_scenario == "missing_payment_id":
            payment_intent_id = None  # Missing payment ID

        # Attempt to create order with invalid payment
        with pytest.raises(expected_exception) as exc_info:
            OrderService.create_order_from_cart(
                cart=cart,
                shipping_address={
                    "first_name": "John",
                    "last_name": "Doe",
                    "email": "john@example.com",
                    "street": "Main St",
                    "street_number": "123",
                    "city": "Test City",
                    "zipcode": "12345",
                    "country_id": 1,
                    "phone": "1234567890",
                },
                payment_intent_id=payment_intent_id,
                pay_way=None,
                user=None,
            )

        # Verify exception is raised
        exception = exc_info.value
        assert isinstance(exception, expected_exception), (
            f"Should raise {expected_exception.__name__}"
        )

        # Verify exception message mentions payment
        error_message = str(exception)
        assert "payment" in error_message.lower(), (
            "Error message should mention payment"
        )

    # ========================================================================
    # Exception Hierarchy Tests
    # ========================================================================

    @pytest.mark.parametrize(
        "specific_exception,base_exception,description",
        [
            # Stock exceptions
            (
                InsufficientStockError,
                StockError,
                "insufficient_stock_is_stock_error",
            ),
            (
                InsufficientStockError,
                OrderServiceError,
                "insufficient_stock_is_order_service_error",
            ),
            (
                StockReservationError,
                StockError,
                "reservation_error_is_stock_error",
            ),
            (
                StockReservationError,
                OrderServiceError,
                "reservation_error_is_order_service_error",
            ),
            # Product exceptions
            (
                ProductNotFoundError,
                OrderServiceError,
                "product_not_found_is_order_service_error",
            ),
            # Order exceptions
            (
                InvalidOrderDataError,
                OrderServiceError,
                "invalid_data_is_order_service_error",
            ),
            (
                OrderNotFoundError,
                OrderServiceError,
                "order_not_found_is_order_service_error",
            ),
            (
                InvalidStatusTransitionError,
                OrderServiceError,
                "invalid_transition_is_order_service_error",
            ),
            (
                OrderCancellationError,
                OrderServiceError,
                "cancellation_error_is_order_service_error",
            ),
            # Payment exceptions
            (
                PaymentNotFoundError,
                PaymentError,
                "payment_not_found_is_payment_error",
            ),
            (
                PaymentNotFoundError,
                OrderServiceError,
                "payment_not_found_is_order_service_error",
            ),
            (
                PaymentVerificationError,
                PaymentError,
                "payment_verification_is_payment_error",
            ),
            (
                PaymentVerificationError,
                OrderServiceError,
                "payment_verification_is_order_service_error",
            ),
            (
                WebhookVerificationError,
                PaymentError,
                "webhook_verification_is_payment_error",
            ),
            (
                WebhookVerificationError,
                OrderServiceError,
                "webhook_verification_is_order_service_error",
            ),
        ],
        ids=[
            "insufficient_stock_is_stock_error",
            "insufficient_stock_is_order_service_error",
            "reservation_error_is_stock_error",
            "reservation_error_is_order_service_error",
            "product_not_found_is_order_service_error",
            "invalid_data_is_order_service_error",
            "order_not_found_is_order_service_error",
            "invalid_transition_is_order_service_error",
            "cancellation_error_is_order_service_error",
            "payment_not_found_is_payment_error",
            "payment_not_found_is_order_service_error",
            "payment_verification_is_payment_error",
            "payment_verification_is_order_service_error",
            "webhook_verification_is_payment_error",
            "webhook_verification_is_order_service_error",
        ],
    )
    def test_exception_hierarchy_allows_catching_as_base_types(
        self, specific_exception, base_exception, description
    ):
        """
        Test that specific exceptions can be caught as their base types.

        Verifies that the exception hierarchy is properly implemented,
        allowing specific exceptions to be caught as their base types.
        """
        # Create appropriate exception instance based on type
        if specific_exception == InsufficientStockError:
            exc = InsufficientStockError(product_id=1, available=0, requested=5)
        elif specific_exception == StockReservationError:
            exc = StockReservationError(
                "Test reservation error", reservation_id=1
            )
        elif specific_exception == ProductNotFoundError:
            exc = ProductNotFoundError(product_id=1)
        elif specific_exception == InvalidOrderDataError:
            exc = InvalidOrderDataError("Test invalid data")
        elif specific_exception == OrderNotFoundError:
            exc = OrderNotFoundError(order_id=1)
        elif specific_exception == InvalidStatusTransitionError:
            exc = InvalidStatusTransitionError(
                current_status="PENDING",
                new_status="SHIPPED",
                allowed=["PROCESSING"],
            )
        elif specific_exception == OrderCancellationError:
            exc = OrderCancellationError(order_id=1, reason="Test reason")
        elif specific_exception == PaymentNotFoundError:
            exc = PaymentNotFoundError(payment_id="pi_test")
        elif specific_exception == PaymentVerificationError:
            exc = PaymentVerificationError(
                payment_id="pi_test", reason="Test reason"
            )
        elif specific_exception == WebhookVerificationError:
            exc = WebhookVerificationError(
                provider="stripe", reason="Test reason"
            )
        else:
            pytest.fail(f"Unknown exception type: {specific_exception}")

        # Verify exception can be caught as base type
        with pytest.raises(base_exception):
            raise exc

        # Verify exception is instance of base type
        assert isinstance(exc, base_exception), (
            f"{specific_exception.__name__} should be instance of {base_exception.__name__}"
        )

    # ========================================================================
    # Exception Context Tests
    # ========================================================================

    def test_insufficient_stock_error_contains_correct_context(self):
        """
        Test that InsufficientStockError contains all required context.
        """
        product_id = 123
        available = 5
        requested = 10

        exc = InsufficientStockError(
            product_id=product_id, available=available, requested=requested
        )

        assert exc.product_id == product_id
        assert exc.available == available
        assert exc.requested == requested
        assert str(product_id) in str(exc)
        assert str(available) in str(exc)
        assert str(requested) in str(exc)

    def test_invalid_status_transition_error_contains_correct_context(self):
        """
        Test that InvalidStatusTransitionError contains all required context.
        """
        current_status = "SHIPPED"
        new_status = "PROCESSING"
        allowed = ["DELIVERED", "RETURNED"]

        exc = InvalidStatusTransitionError(
            current_status=current_status,
            new_status=new_status,
            allowed=allowed,
        )

        assert exc.current_status == current_status
        assert exc.new_status == new_status
        assert exc.allowed == allowed
        assert current_status in str(exc)
        assert new_status in str(exc)

    def test_product_not_found_error_contains_correct_context(self):
        """
        Test that ProductNotFoundError contains all required context.
        """
        product_id = 456

        exc = ProductNotFoundError(product_id=product_id)

        assert exc.product_id == product_id
        assert str(product_id) in str(exc)
        assert "not found" in str(exc).lower()

    def test_payment_not_found_error_contains_correct_context(self):
        """
        Test that PaymentNotFoundError contains all required context.
        """
        payment_id = "pi_test_12345"

        exc = PaymentNotFoundError(payment_id=payment_id)

        assert exc.payment_id == payment_id
        assert payment_id in str(exc)
        assert "not found" in str(exc).lower()

    def test_order_cancellation_error_contains_correct_context(self):
        """
        Test that OrderCancellationError contains all required context.
        """
        order_id = 789
        reason = "Order already shipped"

        exc = OrderCancellationError(order_id=order_id, reason=reason)

        assert exc.order_id == order_id
        assert exc.reason == reason
        assert str(order_id) in str(exc)
        assert reason in str(exc)
