"""
Unit tests for EntitlementService.

These tests use mocks to avoid database overhead. The EntitlementService
resolves effective entitlements for a business by combining:
1. TenantCustomTier (if exists and is_active) - replaces plan features
2. Base plan features (from Subscription.plan_version, if no custom tier)
3. Add-on features (from active SubscriptionAddOns, ADDED on top)

Resolution order:
1. TenantCustomTier (if exists and is_active) OR Base PlanVersion features
2. Active, non-expired SubscriptionAddOn features (ADDED to base)
"""

from datetime import timedelta
from unittest.mock import MagicMock
from unittest.mock import Mock
from unittest.mock import patch

import pytest
from django.utils import timezone


# =============================================================================
# Helper Functions
# =============================================================================


def create_mock_feature(code: str, feature_type: str = "boolean"):
    """Create a mock Feature object."""
    feature = Mock()
    feature.code = code
    feature.feature_type = feature_type
    return feature


def create_mock_plan_feature(feature_code: str, value, feature_type: str = "boolean"):
    """Create a mock PlanFeature object."""
    pf = Mock()
    pf.feature = create_mock_feature(feature_code, feature_type)
    if feature_type == "boolean":
        pf.bool_value = value
        pf.int_value = None
    else:
        pf.bool_value = None
        pf.int_value = value
    pf.get_value = Mock(return_value=value)
    return pf


def create_mock_addon_feature(feature_code: str, value, feature_type: str = "boolean"):
    """Create a mock AddOnFeature object."""
    af = Mock()
    af.feature = create_mock_feature(feature_code, feature_type)
    if feature_type == "boolean":
        af.bool_value = value
        af.int_value = None
    else:
        af.bool_value = None
        af.int_value = value
    af.get_value = Mock(return_value=value)
    return af


def create_mock_custom_tier(features_dict: dict, is_active: bool = True):
    """Create a mock TenantCustomTier object."""
    custom_tier = Mock()
    custom_tier.features = features_dict
    custom_tier.is_active = is_active
    return custom_tier


def create_mock_subscription_addon(
    addon_features: list,
    status: str = "active",
    expires_at=None,
    is_stackable: bool = False,
    quantity: int = 1,
):
    """Create a mock SubscriptionAddOn object."""
    sa = Mock()
    sa.status = status
    sa.expires_at = expires_at
    sa.quantity = quantity
    sa.addon = Mock()
    sa.addon.is_stackable = is_stackable
    sa.addon.features = Mock()
    # The service uses select_related("feature").all()
    sa.addon.features.select_related = Mock(
        return_value=Mock(all=Mock(return_value=addon_features))
    )
    # Calculate is_active
    if status == "active" and (expires_at is None or expires_at > timezone.now()):
        sa.is_active = True
    else:
        sa.is_active = False
    return sa


# =============================================================================
# get_effective_entitlements() Tests
# =============================================================================


class TestGetEffectiveEntitlements:
    """Tests for EntitlementService.get_effective_entitlements()."""

    def test_returns_empty_dict_when_no_subscription(self):
        """Should return empty dict when business has no subscription."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()
        mock_business.billing_subscription = None

        result = EntitlementService.get_effective_entitlements(mock_business)

        assert result == {}

    def test_returns_base_plan_features(self):
        """Should return features from the base plan when no add-ons or custom tier."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        # Set up mock subscription with plan features
        mock_business = Mock()
        mock_subscription = Mock()
        mock_subscription.status = "active"
        mock_subscription.is_active = True
        mock_subscription.plan_version = Mock()
        mock_subscription.plan_version.features = Mock()
        mock_subscription.plan_version.features.select_related = Mock(
            return_value=Mock(
                all=Mock(
                    return_value=[
                        create_mock_plan_feature("sms", True),
                        create_mock_plan_feature("max_users", 5, "integer"),
                    ]
                )
            )
        )
        mock_subscription.addons = Mock()
        mock_subscription.addons.filter = Mock(
            return_value=Mock(
                select_related=Mock(
                    return_value=Mock(all=Mock(return_value=[]))
                )
            )
        )
        mock_business.billing_subscription = mock_subscription
        mock_business.custom_tier = None  # No custom tier

        result = EntitlementService.get_effective_entitlements(mock_business)

        assert result["sms"] is True
        assert result["max_users"] == 5

    def test_addon_features_stack_on_plan_features(self):
        """Add-on features should be added to the result alongside plan features."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()
        mock_subscription = Mock()
        mock_subscription.status = "active"
        mock_subscription.is_active = True
        mock_subscription.plan_version = Mock()
        mock_subscription.plan_version.features = Mock()
        mock_subscription.plan_version.features.select_related = Mock(
            return_value=Mock(
                all=Mock(
                    return_value=[
                        create_mock_plan_feature("sms", True),
                    ]
                )
            )
        )

        # Add-on with additional feature
        addon_with_extra = create_mock_subscription_addon(
            addon_features=[
                create_mock_addon_feature("advanced_reporting", True),
            ],
            status="active",
        )
        mock_subscription.addons = Mock()
        mock_subscription.addons.filter = Mock(
            return_value=Mock(
                select_related=Mock(
                    return_value=Mock(all=Mock(return_value=[addon_with_extra]))
                )
            )
        )
        mock_business.billing_subscription = mock_subscription
        mock_business.custom_tier = None  # No custom tier

        result = EntitlementService.get_effective_entitlements(mock_business)

        assert result["sms"] is True
        assert result["advanced_reporting"] is True

    def test_custom_tier_replaces_plan_features(self):
        """TenantCustomTier should replace plan features when active."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()
        mock_subscription = Mock()
        mock_subscription.status = "active"
        mock_subscription.is_active = True
        mock_subscription.plan_version = Mock()
        mock_subscription.plan_version.features = Mock()
        mock_subscription.plan_version.features.select_related = Mock(
            return_value=Mock(
                all=Mock(
                    return_value=[
                        # Plan says max_users = 5, sms = False
                        create_mock_plan_feature("max_users", 5, "integer"),
                        create_mock_plan_feature("sms", False),
                    ]
                )
            )
        )
        mock_subscription.addons = Mock()
        mock_subscription.addons.filter = Mock(
            return_value=Mock(
                select_related=Mock(
                    return_value=Mock(all=Mock(return_value=[]))
                )
            )
        )
        mock_business.billing_subscription = mock_subscription

        # Custom tier gives different values
        mock_business.custom_tier = create_mock_custom_tier({
            "max_users": 100,
            "sms": True,
            "advanced_reporting": True,
        })

        result = EntitlementService.get_effective_entitlements(mock_business)

        # Custom tier replaces plan features
        assert result["max_users"] == 100
        assert result["sms"] is True
        assert result["advanced_reporting"] is True

    def test_inactive_custom_tier_is_ignored(self):
        """Inactive custom tier should be ignored, falling back to plan."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()
        mock_subscription = Mock()
        mock_subscription.status = "active"
        mock_subscription.is_active = True
        mock_subscription.plan_version = Mock()
        mock_subscription.plan_version.features = Mock()
        mock_subscription.plan_version.features.select_related = Mock(
            return_value=Mock(
                all=Mock(
                    return_value=[
                        create_mock_plan_feature("sms", True),
                        create_mock_plan_feature("max_users", 10, "integer"),
                    ]
                )
            )
        )
        mock_subscription.addons = Mock()
        mock_subscription.addons.filter = Mock(
            return_value=Mock(
                select_related=Mock(
                    return_value=Mock(all=Mock(return_value=[]))
                )
            )
        )
        mock_business.billing_subscription = mock_subscription

        # Inactive custom tier (grace period expired)
        mock_business.custom_tier = create_mock_custom_tier(
            {"sms": False, "max_users": 100},
            is_active=False
        )

        result = EntitlementService.get_effective_entitlements(mock_business)

        # Inactive custom tier is ignored, plan values win
        assert result["sms"] is True
        assert result["max_users"] == 10

    def test_expired_addon_is_ignored(self):
        """Expired add-ons should not affect the result."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()
        mock_subscription = Mock()
        mock_subscription.status = "active"
        mock_subscription.is_active = True
        mock_subscription.plan_version = Mock()
        mock_subscription.plan_version.features = Mock()
        mock_subscription.plan_version.features.select_related = Mock(
            return_value=Mock(all=Mock(return_value=[]))
        )

        # Expired add-on
        expired_addon = create_mock_subscription_addon(
            addon_features=[
                create_mock_addon_feature("advanced_reporting", True),
            ],
            status="active",
            expires_at=timezone.now() - timedelta(days=1),
        )
        mock_subscription.addons = Mock()
        mock_subscription.addons.filter = Mock(
            return_value=Mock(
                select_related=Mock(
                    return_value=Mock(all=Mock(return_value=[expired_addon]))
                )
            )
        )
        mock_business.billing_subscription = mock_subscription
        mock_business.custom_tier = None  # No custom tier

        result = EntitlementService.get_effective_entitlements(mock_business)

        # Expired add-on is ignored
        assert "advanced_reporting" not in result

    def test_canceled_addon_is_ignored(self):
        """Canceled add-ons should not affect the result."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()
        mock_subscription = Mock()
        mock_subscription.status = "active"
        mock_subscription.is_active = True
        mock_subscription.plan_version = Mock()
        mock_subscription.plan_version.features = Mock()
        mock_subscription.plan_version.features.select_related = Mock(
            return_value=Mock(all=Mock(return_value=[]))
        )

        # Canceled add-on
        canceled_addon = create_mock_subscription_addon(
            addon_features=[
                create_mock_addon_feature("advanced_reporting", True),
            ],
            status="canceled",
        )
        mock_subscription.addons = Mock()
        mock_subscription.addons.filter = Mock(
            return_value=Mock(
                select_related=Mock(
                    return_value=Mock(all=Mock(return_value=[canceled_addon]))
                )
            )
        )
        mock_business.billing_subscription = mock_subscription
        mock_business.custom_tier = None  # No custom tier

        result = EntitlementService.get_effective_entitlements(mock_business)

        # Canceled add-on is ignored
        assert "advanced_reporting" not in result

    def test_integer_limits_addon_adds_to_plan(self):
        """Integer add-on values should ADD to base plan value, not replace."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()
        mock_subscription = Mock()
        mock_subscription.status = "active"
        mock_subscription.is_active = True
        mock_subscription.plan_version = Mock()
        mock_subscription.plan_version.features = Mock()
        mock_subscription.plan_version.features.select_related = Mock(
            return_value=Mock(
                all=Mock(
                    return_value=[
                        # Plan gives 10 users
                        create_mock_plan_feature("max_users", 10, "integer"),
                    ]
                )
            )
        )

        # Add-on gives 25 more users (should ADD to base plan)
        addon = create_mock_subscription_addon(
            addon_features=[
                create_mock_addon_feature("max_users", 25, "integer"),
            ],
            status="active",
        )
        mock_subscription.addons = Mock()
        mock_subscription.addons.filter = Mock(
            return_value=Mock(
                select_related=Mock(
                    return_value=Mock(all=Mock(return_value=[addon]))
                )
            )
        )
        mock_business.billing_subscription = mock_subscription
        mock_business.custom_tier = None  # No custom tier

        result = EntitlementService.get_effective_entitlements(mock_business)

        # Add-on values ADD to plan values: 10 + 25 = 35
        assert result["max_users"] == 35

    def test_addon_adds_to_custom_tier(self):
        """Integer add-on values should ADD to custom tier values."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()
        mock_subscription = Mock()
        mock_subscription.status = "active"
        mock_subscription.is_active = True
        mock_subscription.plan_version = Mock()
        mock_subscription.plan_version.features = Mock()
        mock_subscription.plan_version.features.select_related = Mock(
            return_value=Mock(all=Mock(return_value=[]))
        )

        # Custom tier gives 50 users (plan features are ignored)
        mock_business.custom_tier = create_mock_custom_tier({
            "max_users": 50,
            "sms": True,
        })

        # Add-on gives 25 more users (should ADD to custom tier)
        addon = create_mock_subscription_addon(
            addon_features=[
                create_mock_addon_feature("max_users", 25, "integer"),
            ],
            status="active",
        )
        mock_subscription.addons = Mock()
        mock_subscription.addons.filter = Mock(
            return_value=Mock(
                select_related=Mock(
                    return_value=Mock(all=Mock(return_value=[addon]))
                )
            )
        )
        mock_business.billing_subscription = mock_subscription

        result = EntitlementService.get_effective_entitlements(mock_business)

        # Add-on values ADD to custom tier values: 50 + 25 = 75
        assert result["max_users"] == 75
        assert result["sms"] is True

    def test_returns_empty_when_subscription_not_active(self):
        """Should return empty dict when subscription is not active."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()
        mock_subscription = Mock()
        mock_subscription.status = "canceled"
        mock_subscription.is_active = False
        mock_business.billing_subscription = mock_subscription

        result = EntitlementService.get_effective_entitlements(mock_business)

        assert result == {}


# =============================================================================
# has_feature() Tests
# =============================================================================


class TestHasFeature:
    """Tests for EntitlementService.has_feature()."""

    def test_returns_true_for_enabled_boolean_feature(self):
        """has_feature should return True when feature is enabled."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()

        with patch.object(
            EntitlementService,
            "get_effective_entitlements",
            return_value={"sms": True, "email": True},
        ):
            assert EntitlementService.has_feature(mock_business, "sms") is True
            assert EntitlementService.has_feature(mock_business, "email") is True

    def test_returns_false_for_disabled_boolean_feature(self):
        """has_feature should return False when feature is disabled."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()

        with patch.object(
            EntitlementService,
            "get_effective_entitlements",
            return_value={"sms": False},
        ):
            assert EntitlementService.has_feature(mock_business, "sms") is False

    def test_returns_false_for_missing_feature(self):
        """has_feature should return False when feature is not in entitlements."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()

        with patch.object(
            EntitlementService,
            "get_effective_entitlements",
            return_value={"sms": True},
        ):
            assert EntitlementService.has_feature(mock_business, "webhooks") is False

    def test_returns_true_for_non_zero_integer_feature(self):
        """has_feature should return True for non-zero integer limits."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()

        with patch.object(
            EntitlementService,
            "get_effective_entitlements",
            return_value={"max_users": 10},
        ):
            assert EntitlementService.has_feature(mock_business, "max_users") is True

    def test_returns_false_for_zero_integer_feature(self):
        """has_feature should return False for zero integer limits."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()

        with patch.object(
            EntitlementService,
            "get_effective_entitlements",
            return_value={"max_users": 0},
        ):
            assert EntitlementService.has_feature(mock_business, "max_users") is False


# =============================================================================
# get_limit() Tests
# =============================================================================


class TestGetLimit:
    """Tests for EntitlementService.get_limit()."""

    def test_returns_integer_value(self):
        """get_limit should return the integer value for integer features."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()

        with patch.object(
            EntitlementService,
            "get_effective_entitlements",
            return_value={"max_users": 50, "max_resources": 100},
        ):
            assert EntitlementService.get_limit(mock_business, "max_users") == 50
            assert EntitlementService.get_limit(mock_business, "max_resources") == 100

    def test_returns_none_for_missing_feature(self):
        """get_limit should return None for missing features."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()

        with patch.object(
            EntitlementService,
            "get_effective_entitlements",
            return_value={"max_users": 50},
        ):
            assert EntitlementService.get_limit(mock_business, "max_widgets") is None

    def test_returns_none_for_boolean_feature(self):
        """get_limit should return None for boolean features."""
        from smoothschedule.billing.services.entitlements import (
            EntitlementService,
        )

        mock_business = Mock()

        with patch.object(
            EntitlementService,
            "get_effective_entitlements",
            return_value={"sms": True},
        ):
            # Boolean values should not be treated as limits
            result = EntitlementService.get_limit(mock_business, "sms")
            assert result is None
