# app/modules/billing/tests/unit/test_admin_subscription_service.py
"""Unit tests for AdminSubscriptionService (admin tier & subscription management)."""

from datetime import UTC, datetime, timedelta

import pytest

from app.exceptions import (
    BusinessLogicException,
    ConflictException,
    ResourceNotFoundException,
)
from app.modules.billing.exceptions import TierNotFoundException
from app.modules.billing.models import (
    BillingHistory,
    MerchantSubscription,
    SubscriptionStatus,
    SubscriptionTier,
)
from app.modules.billing.services.admin_subscription_service import (
    AdminSubscriptionService,
)
from app.modules.tenancy.models import Merchant

# ============================================================================
# Tier Management
# ============================================================================


@pytest.mark.unit
@pytest.mark.billing
class TestAdminGetTiers:
    """Tests for get_tiers with filtering."""

    def setup_method(self):
        self.service = AdminSubscriptionService()

    def test_get_tiers_active_only_by_default(self, db, admin_billing_tiers):
        """Default call returns only active tiers."""
        tiers = self.service.get_tiers(db)

        assert len(tiers) == 3
        assert all(t.is_active for t in tiers)

    def test_get_tiers_include_inactive(self, db, admin_billing_tiers):
        """include_inactive=True returns all tiers."""
        admin_billing_tiers[0].is_active = False
        db.flush()

        tiers = self.service.get_tiers(db, include_inactive=True)
        assert len(tiers) == 3

    def test_get_tiers_active_excludes_inactive(self, db, admin_billing_tiers):
        """Active filter excludes inactive tiers."""
        admin_billing_tiers[0].is_active = False
        db.flush()

        tiers = self.service.get_tiers(db, include_inactive=False)
        assert len(tiers) == 2
        assert all(t.code != "essential" for t in tiers)

    def test_get_tiers_ordered_by_display_order(self, db, admin_billing_tiers):
        """Tiers are returned in display_order."""
        tiers = self.service.get_tiers(db)
        orders = [t.display_order for t in tiers]
        assert orders == sorted(orders)

    def test_get_tiers_platform_filter(self, db, test_platform, admin_billing_tiers):
        """Platform filter returns platform-specific + global tiers."""
        admin_billing_tiers[0].platform_id = test_platform.id
        db.flush()

        tiers = self.service.get_tiers(db, platform_id=test_platform.id)
        codes = {t.code for t in tiers}
        assert "essential" in codes  # Platform-specific
        assert "professional" in codes  # Global
        assert "business" in codes  # Global

    def test_get_tiers_platform_filter_excludes_other(
        self, db, test_platform, another_platform, admin_billing_tiers
    ):
        """Tiers from another platform are excluded."""
        admin_billing_tiers[0].platform_id = another_platform.id
        db.flush()

        tiers = self.service.get_tiers(db, platform_id=test_platform.id)
        assert all(t.code != "essential" for t in tiers)


@pytest.mark.unit
@pytest.mark.billing
class TestAdminGetTierByCode:
    """Tests for get_tier_by_code."""

    def setup_method(self):
        self.service = AdminSubscriptionService()

    def test_get_tier_by_code_found(self, db, admin_billing_tiers):
        """Returns the tier when it exists."""
        tier = self.service.get_tier_by_code(db, "professional")
        assert tier.code == "professional"
        assert tier.name == "Professional"

    def test_get_tier_by_code_not_found_raises(self, db):
        """Raises TierNotFoundException for nonexistent code."""
        with pytest.raises(TierNotFoundException):
            self.service.get_tier_by_code(db, "nonexistent")


@pytest.mark.unit
@pytest.mark.billing
class TestAdminCreateTier:
    """Tests for create_tier."""

    def setup_method(self):
        self.service = AdminSubscriptionService()

    def test_create_tier_success(self, db):
        """Creates a new tier with provided data."""
        data = {
            "code": "starter",
            "name": "Starter",
            "description": "A starter plan",
            "price_monthly_cents": 1900,
            "price_annual_cents": 19000,
            "display_order": 0,
            "is_active": True,
            "is_public": True,
        }
        tier = self.service.create_tier(db, data)
        db.flush()

        assert tier.code == "starter"
        assert tier.name == "Starter"
        assert tier.price_monthly_cents == 1900

    def test_create_tier_duplicate_raises(self, db, admin_billing_tiers):
        """Duplicate tier code raises ConflictException."""
        data = {
            "code": "essential",
            "name": "Essential Duplicate",
            "price_monthly_cents": 4900,
            "display_order": 1,
            "is_active": True,
            "is_public": True,
        }
        with pytest.raises(ConflictException, match="already exists"):
            self.service.create_tier(db, data)


@pytest.mark.unit
@pytest.mark.billing
class TestAdminUpdateTier:
    """Tests for update_tier."""

    def setup_method(self):
        self.service = AdminSubscriptionService()

    def test_update_tier_name(self, db, admin_billing_tiers):
        """Updates tier fields."""
        tier = self.service.update_tier(db, "essential", {"name": "Essential Plus"})
        assert tier.name == "Essential Plus"

    def test_update_tier_price(self, db, admin_billing_tiers):
        """Updates tier pricing."""
        tier = self.service.update_tier(
            db, "essential", {"price_monthly_cents": 5900}
        )
        assert tier.price_monthly_cents == 5900

    def test_update_tier_nonexistent_raises(self, db):
        """Updating a nonexistent tier raises."""
        with pytest.raises(TierNotFoundException):
            self.service.update_tier(db, "nonexistent", {"name": "X"})


@pytest.mark.unit
@pytest.mark.billing
class TestAdminDeactivateTier:
    """Tests for deactivate_tier (soft-delete)."""

    def setup_method(self):
        self.service = AdminSubscriptionService()

    def test_deactivate_tier_sets_inactive(self, db, admin_billing_tiers):
        """Deactivation sets is_active to False."""
        self.service.deactivate_tier(db, "business")
        db.flush()

        tier = db.query(SubscriptionTier).filter(
            SubscriptionTier.code == "business"
        ).first()
        assert tier.is_active is False

    def test_deactivate_tier_with_active_subs_raises(
        self, db, admin_billing_tiers, admin_billing_subscription
    ):
        """Cannot deactivate a tier used by active subscriptions."""
        with pytest.raises(BusinessLogicException, match="active subscriptions"):
            self.service.deactivate_tier(db, "essential")

    def test_deactivate_tier_with_cancelled_subs_ok(
        self, db, admin_billing_tiers, admin_billing_subscription
    ):
        """Can deactivate a tier used only by cancelled subscriptions."""
        admin_billing_subscription.status = SubscriptionStatus.CANCELLED.value
        db.flush()

        # Should not raise
        self.service.deactivate_tier(db, "essential")

    def test_deactivate_nonexistent_tier_raises(self, db):
        """Deactivating a nonexistent tier raises."""
        with pytest.raises(TierNotFoundException):
            self.service.deactivate_tier(db, "nonexistent")


# ============================================================================
# Subscription Management
# ============================================================================


@pytest.mark.unit
@pytest.mark.billing
class TestAdminListSubscriptions:
    """Tests for list_subscriptions with pagination and filtering."""

    def setup_method(self):
        self.service = AdminSubscriptionService()

    def test_list_subscriptions_empty(self, db):
        """Returns empty results when no subscriptions exist."""
        result = self.service.list_subscriptions(db)
        assert result["total"] == 0
        assert result["results"] == []
        assert result["page"] == 1

    def test_list_subscriptions_returns_results(
        self, db, admin_billing_subscription
    ):
        """Returns subscriptions joined with merchant."""
        result = self.service.list_subscriptions(db)
        assert result["total"] == 1
        sub, merchant = result["results"][0]
        assert isinstance(sub, MerchantSubscription)
        assert isinstance(merchant, Merchant)

    def test_list_subscriptions_filter_by_status(
        self, db, admin_billing_subscription, admin_billing_subscription_trial
    ):
        """Status filter returns only matching subscriptions."""
        result = self.service.list_subscriptions(db, status="trial")
        assert result["total"] == 1
        sub, _ = result["results"][0]
        assert sub.status == "trial"

    def test_list_subscriptions_filter_by_tier(
        self, db, admin_billing_tiers, admin_billing_subscription
    ):
        """Tier filter returns only matching subscriptions."""
        result = self.service.list_subscriptions(db, tier="essential")
        assert result["total"] == 1

        result = self.service.list_subscriptions(db, tier="professional")
        assert result["total"] == 0

    def test_list_subscriptions_search_by_merchant_name(
        self, db, admin_billing_subscription, test_merchant
    ):
        """Search filters by merchant name (case-insensitive)."""
        # test_merchant has a name like "Test Merchant xxxxxxxx"
        result = self.service.list_subscriptions(db, search="Test Merchant")
        assert result["total"] == 1

        result = self.service.list_subscriptions(db, search="NonExistent Corp")
        assert result["total"] == 0

    def test_list_subscriptions_pagination(
        self, db, admin_billing_multiple_subscriptions
    ):
        """Pagination returns correct page with correct count."""
        result = self.service.list_subscriptions(db, page=1, per_page=2)
        assert result["total"] == 3
        assert len(result["results"]) == 2
        assert result["pages"] == 2

        result2 = self.service.list_subscriptions(db, page=2, per_page=2)
        assert len(result2["results"]) == 1


@pytest.mark.unit
@pytest.mark.billing
class TestAdminGetSubscription:
    """Tests for get_subscription."""

    def setup_method(self):
        self.service = AdminSubscriptionService()

    def test_get_subscription_found(self, db, admin_billing_subscription):
        """Returns (subscription, merchant) tuple."""
        sub, merchant = self.service.get_subscription(
            db,
            admin_billing_subscription.merchant_id,
            admin_billing_subscription.platform_id,
        )
        assert sub.id == admin_billing_subscription.id
        assert isinstance(merchant, Merchant)

    def test_get_subscription_not_found_raises(self, db):
        """Raises ResourceNotFoundException when not found."""
        with pytest.raises(ResourceNotFoundException):
            self.service.get_subscription(db, 99999, 99999)


@pytest.mark.unit
@pytest.mark.billing
class TestAdminUpdateSubscription:
    """Tests for update_subscription — the tier_code→tier_id resolution bug area."""

    def setup_method(self):
        self.service = AdminSubscriptionService()

    def test_update_subscription_status(self, db, admin_billing_subscription):
        """Can update subscription status."""
        sub, _ = self.service.update_subscription(
            db,
            admin_billing_subscription.merchant_id,
            admin_billing_subscription.platform_id,
            {"status": SubscriptionStatus.PAST_DUE.value},
        )
        assert sub.status == SubscriptionStatus.PAST_DUE.value

    def test_update_subscription_tier_code_resolves_to_tier_id(
        self, db, admin_billing_tiers, admin_billing_subscription
    ):
        """
        tier_code in update_data is resolved to tier_id (FK).

        This is the exact bug that was fixed in commit d1fe358 — the admin form
        sends tier_code (string) but the model uses tier_id (int FK).
        """
        sub, _ = self.service.update_subscription(
            db,
            admin_billing_subscription.merchant_id,
            admin_billing_subscription.platform_id,
            {"tier_code": "professional"},
        )
        assert sub.tier_id == admin_billing_tiers[1].id

    def test_update_subscription_tier_code_nonexistent_raises(
        self, db, admin_billing_subscription
    ):
        """Nonexistent tier_code raises TierNotFoundException."""
        with pytest.raises(TierNotFoundException):
            self.service.update_subscription(
                db,
                admin_billing_subscription.merchant_id,
                admin_billing_subscription.platform_id,
                {"tier_code": "nonexistent"},
            )

    def test_update_subscription_not_found_raises(self, db):
        """Updating a nonexistent subscription raises."""
        with pytest.raises(ResourceNotFoundException):
            self.service.update_subscription(db, 99999, 99999, {"status": "active"})


# ============================================================================
# Billing History
# ============================================================================


@pytest.mark.unit
@pytest.mark.billing
class TestAdminListBillingHistory:
    """Tests for list_billing_history."""

    def setup_method(self):
        self.service = AdminSubscriptionService()

    def test_list_billing_history_empty(self, db):
        """Returns empty when no records exist."""
        result = self.service.list_billing_history(db)
        assert result["total"] == 0
        assert result["results"] == []

    def test_list_billing_history_with_records(
        self, db, admin_billing_history_records
    ):
        """Returns billing history joined with merchant."""
        result = self.service.list_billing_history(db)
        assert result["total"] == 3
        bh, merchant = result["results"][0]
        assert isinstance(bh, BillingHistory)
        assert isinstance(merchant, Merchant)

    def test_list_billing_history_filter_by_merchant(
        self, db, admin_billing_history_records, test_merchant
    ):
        """Merchant filter returns only matching records."""
        result = self.service.list_billing_history(
            db, merchant_id=test_merchant.id
        )
        assert result["total"] == 3

        result = self.service.list_billing_history(db, merchant_id=99999)
        assert result["total"] == 0

    def test_list_billing_history_filter_by_status(
        self, db, admin_billing_history_records
    ):
        """Status filter returns matching records."""
        result = self.service.list_billing_history(db, status="paid")
        assert result["total"] == 3

        result = self.service.list_billing_history(db, status="failed")
        assert result["total"] == 0

    def test_list_billing_history_pagination(
        self, db, admin_billing_history_records
    ):
        """Pagination works correctly."""
        result = self.service.list_billing_history(db, page=1, per_page=2)
        assert result["total"] == 3
        assert len(result["results"]) == 2
        assert result["pages"] == 2


# ============================================================================
# Statistics
# ============================================================================


@pytest.mark.unit
@pytest.mark.billing
class TestAdminGetStats:
    """Tests for get_stats (dashboard analytics)."""

    def setup_method(self):
        self.service = AdminSubscriptionService()

    def test_get_stats_empty_db(self, db):
        """Stats with no subscriptions return zeroes."""
        stats = self.service.get_stats(db)

        assert stats["total_subscriptions"] == 0
        assert stats["active_count"] == 0
        assert stats["trial_count"] == 0
        assert stats["mrr_cents"] == 0
        assert stats["arr_cents"] == 0
        assert stats["tier_distribution"] == {}

    def test_get_stats_counts_by_status(
        self, db, admin_billing_subscription, admin_billing_subscription_trial
    ):
        """Status counts are accurate."""
        stats = self.service.get_stats(db)

        assert stats["total_subscriptions"] == 2
        assert stats["active_count"] == 1
        assert stats["trial_count"] == 1

    def test_get_stats_tier_distribution(
        self, db, admin_billing_tiers, admin_billing_subscription
    ):
        """Tier distribution counts active/trial subscriptions by tier name."""
        stats = self.service.get_stats(db)

        assert "Essential" in stats["tier_distribution"]
        assert stats["tier_distribution"]["Essential"] == 1

    def test_get_stats_mrr_calculation_monthly(
        self, db, admin_billing_tiers, admin_billing_subscription
    ):
        """MRR for a monthly subscription equals the monthly price."""
        stats = self.service.get_stats(db)
        # Essential tier is 4900 cents/month
        assert stats["mrr_cents"] == 4900
        assert stats["arr_cents"] == 4900 * 12

    def test_get_stats_mrr_calculation_annual(
        self, db, admin_billing_tiers, admin_billing_subscription
    ):
        """MRR for an annual subscription is annual_price / 12."""
        admin_billing_subscription.is_annual = True
        db.flush()

        stats = self.service.get_stats(db)
        # Essential annual = 49000 cents → MRR = 49000 / 12 = 4083
        assert stats["mrr_cents"] == 49000 // 12
        assert stats["arr_cents"] == 49000


# ============================================================================
# Fixtures
# ============================================================================


@pytest.fixture
def admin_billing_tiers(db, test_platform):
    """Create essential, professional, business tiers for admin tests."""
    tiers = [
        SubscriptionTier(
            code="essential",
            name="Essential",
            price_monthly_cents=4900,
            price_annual_cents=49000,
            display_order=1,
            is_active=True,
            is_public=True,
            platform_id=test_platform.id,
        ),
        SubscriptionTier(
            code="professional",
            name="Professional",
            price_monthly_cents=9900,
            price_annual_cents=99000,
            display_order=2,
            is_active=True,
            is_public=True,
            platform_id=test_platform.id,
        ),
        SubscriptionTier(
            code="business",
            name="Business",
            price_monthly_cents=19900,
            price_annual_cents=199000,
            display_order=3,
            is_active=True,
            is_public=True,
            platform_id=test_platform.id,
        ),
    ]
    db.add_all(tiers)
    db.commit()
    for t in tiers:
        db.refresh(t)
    return tiers


@pytest.fixture
def admin_billing_subscription(db, test_merchant, test_platform, admin_billing_tiers):
    """Create an active subscription on the essential tier."""
    now = datetime.now(UTC)
    sub = MerchantSubscription(
        merchant_id=test_merchant.id,
        platform_id=test_platform.id,
        tier_id=admin_billing_tiers[0].id,
        status=SubscriptionStatus.ACTIVE.value,
        period_start=now,
        period_end=now + timedelta(days=30),
    )
    db.add(sub)
    db.commit()
    db.refresh(sub)
    return sub


@pytest.fixture
def admin_billing_subscription_trial(
    db, other_merchant, another_platform, admin_billing_tiers
):
    """Create a trial subscription on another merchant/platform."""
    now = datetime.now(UTC)
    sub = MerchantSubscription(
        merchant_id=other_merchant.id,
        platform_id=another_platform.id,
        tier_id=admin_billing_tiers[0].id,
        status=SubscriptionStatus.TRIAL.value,
        period_start=now,
        period_end=now + timedelta(days=14),
        trial_ends_at=now + timedelta(days=14),
    )
    db.add(sub)
    db.commit()
    db.refresh(sub)
    return sub


@pytest.fixture
def admin_billing_multiple_subscriptions(
    db, test_merchant, other_merchant, test_platform, another_platform,
    admin_billing_tiers, platform_factory,
):
    """Create 3 subscriptions across different merchants/platforms."""
    now = datetime.now(UTC)
    third_platform = platform_factory()

    subs = [
        MerchantSubscription(
            merchant_id=test_merchant.id,
            platform_id=test_platform.id,
            tier_id=admin_billing_tiers[0].id,
            status=SubscriptionStatus.ACTIVE.value,
            period_start=now,
            period_end=now + timedelta(days=30),
        ),
        MerchantSubscription(
            merchant_id=other_merchant.id,
            platform_id=another_platform.id,
            tier_id=admin_billing_tiers[1].id,
            status=SubscriptionStatus.TRIAL.value,
            period_start=now,
            period_end=now + timedelta(days=14),
        ),
        MerchantSubscription(
            merchant_id=test_merchant.id,
            platform_id=third_platform.id,
            tier_id=admin_billing_tiers[2].id,
            status=SubscriptionStatus.ACTIVE.value,
            period_start=now,
            period_end=now + timedelta(days=30),
        ),
    ]
    db.add_all(subs)
    db.commit()
    for s in subs:
        db.refresh(s)
    return subs


@pytest.fixture
def admin_billing_history_records(db, test_merchant):
    """Create 3 billing history records for test_merchant."""
    records = []
    for i in range(3):
        record = BillingHistory(
            merchant_id=test_merchant.id,
            stripe_invoice_id=f"in_admin_test_{i}",
            invoice_number=f"ADM-{i:03d}",
            invoice_date=datetime.now(UTC) - timedelta(days=i * 30),
            subtotal_cents=4900,
            tax_cents=0,
            total_cents=4900,
            amount_paid_cents=4900,
            currency="EUR",
            status="paid",
        )
        records.append(record)
    db.add_all(records)
    db.commit()
    for r in records:
        db.refresh(r)
    return records
