"""Unit tests for StampService."""

import uuid
from datetime import UTC, datetime, timedelta

import pytest

from app.modules.loyalty.exceptions import (
    DailyStampLimitException,
    InsufficientStampsException,
    StampCooldownException,
)
from app.modules.loyalty.models import LoyaltyCard, LoyaltyProgram, LoyaltyTransaction
from app.modules.loyalty.models.loyalty_program import LoyaltyType
from app.modules.loyalty.models.loyalty_transaction import TransactionType
from app.modules.loyalty.services.stamp_service import StampService
from app.modules.tenancy.models import Merchant, Store, User
from app.modules.tenancy.models.store import StoreUser


@pytest.mark.unit
@pytest.mark.loyalty
class TestStampService:
    """Test suite for StampService."""

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

    def test_service_instantiation(self):
        """Service can be instantiated."""
        assert self.service is not None


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


@pytest.fixture
def stamp_setup(db):
    """Create a full setup for stamp tests with stamps-type program."""
    from app.modules.customers.models.customer import Customer
    from middleware.auth import AuthManager

    auth = AuthManager()
    uid = uuid.uuid4().hex[:8]

    owner = User(
        email=f"stampowner_{uid}@test.com",
        username=f"stampowner_{uid}",
        hashed_password=auth.hash_password("testpass"),
        role="merchant_owner",
        is_active=True,
        is_email_verified=True,
    )
    db.add(owner)
    db.commit()
    db.refresh(owner)

    merchant = Merchant(
        name=f"Stamp Merchant {uid}",
        owner_user_id=owner.id,
        contact_email=owner.email,
        is_active=True,
        is_verified=True,
    )
    db.add(merchant)
    db.commit()
    db.refresh(merchant)

    store = Store(
        merchant_id=merchant.id,
        store_code=f"STAMP_{uid.upper()}",
        subdomain=f"stamp{uid}",
        name=f"Stamp Store {uid}",
        is_active=True,
        is_verified=True,
    )
    db.add(store)
    db.commit()
    db.refresh(store)

    store_user = StoreUser(store_id=store.id, user_id=owner.id, is_active=True)
    db.add(store_user)
    db.commit()

    customer = Customer(
        email=f"stampcust_{uid}@test.com",
        first_name="Stamp",
        last_name="Customer",
        hashed_password="!unused!",  # noqa: SEC001
        customer_number=f"SC-{uid.upper()}",
        store_id=store.id,
        is_active=True,
    )
    db.add(customer)
    db.commit()
    db.refresh(customer)

    program = LoyaltyProgram(
        merchant_id=merchant.id,
        loyalty_type=LoyaltyType.STAMPS.value,
        stamps_target=5,
        stamps_reward_description="Free coffee",
        stamps_reward_value_cents=500,
        cooldown_minutes=0,
        max_daily_stamps=10,
        require_staff_pin=False,
        card_name="Stamp Card",
        card_color="#FF0000",
        is_active=True,
        points_per_euro=1,
    )
    db.add(program)
    db.commit()
    db.refresh(program)

    card = LoyaltyCard(
        merchant_id=merchant.id,
        program_id=program.id,
        customer_id=customer.id,
        enrolled_at_store_id=store.id,
        card_number=f"STAMPCARD-{uid.upper()}",
        stamp_count=0,
        total_stamps_earned=0,
        stamps_redeemed=0,
        points_balance=0,
        total_points_earned=0,
        points_redeemed=0,
        is_active=True,
        last_activity_at=datetime.now(UTC),
    )
    db.add(card)
    db.commit()
    db.refresh(card)

    return {
        "merchant": merchant,
        "store": store,
        "customer": customer,
        "program": program,
        "card": card,
    }


# ============================================================================
# Add Stamp Tests
# ============================================================================


@pytest.mark.unit
@pytest.mark.loyalty
class TestAddStamp:
    """Tests for add_stamp."""

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

    def test_add_stamp_success(self, db, stamp_setup):
        """Successfully add a stamp to a card."""
        card = stamp_setup["card"]
        store = stamp_setup["store"]

        result = self.service.add_stamp(db, store_id=store.id, card_id=card.id)

        assert result["success"] is True
        assert result["stamp_count"] == 1
        assert result["stamps_target"] == 5
        assert result["stamps_until_reward"] == 4

    def test_add_stamp_cooldown_violation(self, db, stamp_setup):
        """Stamp within cooldown period raises exception."""
        card = stamp_setup["card"]
        store = stamp_setup["store"]
        program = stamp_setup["program"]

        # Set cooldown
        program.cooldown_minutes = 15
        db.commit()

        # Add first stamp
        self.service.add_stamp(db, store_id=store.id, card_id=card.id)

        # Second stamp should fail (cooldown)
        with pytest.raises(StampCooldownException):
            self.service.add_stamp(db, store_id=store.id, card_id=card.id)

    def test_add_stamp_daily_limit(self, db, stamp_setup):
        """Exceeding daily stamp limit raises exception."""
        card = stamp_setup["card"]
        store = stamp_setup["store"]
        program = stamp_setup["program"]

        # Set max 2 daily stamps
        program.max_daily_stamps = 2
        db.commit()

        # Add 2 stamps
        self.service.add_stamp(db, store_id=store.id, card_id=card.id)
        self.service.add_stamp(db, store_id=store.id, card_id=card.id)

        # Third should fail
        with pytest.raises(DailyStampLimitException):
            self.service.add_stamp(db, store_id=store.id, card_id=card.id)


# ============================================================================
# Redeem Stamps Tests
# ============================================================================


@pytest.mark.unit
@pytest.mark.loyalty
class TestRedeemStamps:
    """Tests for redeem_stamps."""

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

    def test_redeem_stamps_success(self, db, stamp_setup):
        """Successfully redeem stamps for a reward."""
        card = stamp_setup["card"]
        store = stamp_setup["store"]
        program = stamp_setup["program"]

        # Give enough stamps
        card.stamp_count = program.stamps_target
        card.total_stamps_earned = program.stamps_target
        db.commit()

        result = self.service.redeem_stamps(db, store_id=store.id, card_id=card.id)

        assert result["success"] is True
        assert result["stamp_count"] == 0
        assert result["reward_description"] == "Free coffee"

    def test_redeem_stamps_insufficient(self, db, stamp_setup):
        """Redeeming without enough stamps raises exception."""
        card = stamp_setup["card"]
        store = stamp_setup["store"]

        # Card has 0 stamps, needs 5
        with pytest.raises(InsufficientStampsException):
            self.service.redeem_stamps(db, store_id=store.id, card_id=card.id)


# ============================================================================
# Void Stamps Tests
# ============================================================================


@pytest.mark.unit
@pytest.mark.loyalty
class TestVoidStamps:
    """Tests for void_stamps."""

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

    def test_void_stamps_by_transaction(self, db, stamp_setup):
        """Void stamps by original transaction ID."""
        card = stamp_setup["card"]
        store = stamp_setup["store"]

        # Add a stamp first
        result = self.service.add_stamp(db, store_id=store.id, card_id=card.id)
        assert result["stamp_count"] == 1

        # Find the transaction
        tx = (
            db.query(LoyaltyTransaction)
            .filter(
                LoyaltyTransaction.card_id == card.id,
                LoyaltyTransaction.transaction_type == TransactionType.STAMP_EARNED.value,
            )
            .first()
        )

        void_result = self.service.void_stamps(
            db,
            store_id=store.id,
            card_id=card.id,
            original_transaction_id=tx.id,
        )

        assert void_result["success"] is True
        assert void_result["stamp_count"] == 0


# ============================================================================
# Item 1: TOCTOU Race Condition (stamp checks after lock)
# ============================================================================


@pytest.mark.unit
@pytest.mark.loyalty
class TestStampTOCTOU:
    """Verify cooldown/daily-limit checks happen after row lock."""

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

    def test_cooldown_checked_after_lock(self, db, stamp_setup):
        """Cooldown check uses locked row state."""
        card = stamp_setup["card"]
        store = stamp_setup["store"]
        program = stamp_setup["program"]

        # Enable cooldown
        program.cooldown_minutes = 60
        db.commit()

        # First stamp succeeds
        self.service.add_stamp(db, store_id=store.id, card_id=card.id)

        # Second stamp should fail (cooldown active after lock)
        with pytest.raises(StampCooldownException):
            self.service.add_stamp(db, store_id=store.id, card_id=card.id)

    def test_daily_limit_checked_after_lock(self, db, stamp_setup):
        """Daily limit check uses locked row state."""
        card = stamp_setup["card"]
        store = stamp_setup["store"]
        program = stamp_setup["program"]

        program.max_daily_stamps = 1
        db.commit()

        # First stamp succeeds
        self.service.add_stamp(db, store_id=store.id, card_id=card.id)

        # Second stamp should fail (daily limit after lock)
        with pytest.raises(DailyStampLimitException):
            self.service.add_stamp(db, store_id=store.id, card_id=card.id)

    def test_redeem_stamps_insufficient_after_lock(self, db, stamp_setup):
        """Stamp count check uses locked row state."""
        card = stamp_setup["card"]
        store = stamp_setup["store"]
        program = stamp_setup["program"]

        # Give exact stamp target
        card.stamp_count = program.stamps_target
        card.total_stamps_earned = program.stamps_target
        db.commit()

        # First redeem succeeds
        result = self.service.redeem_stamps(db, store_id=store.id, card_id=card.id)
        assert result["success"] is True

        # Second redeem should fail (0 stamps after lock)
        with pytest.raises(InsufficientStampsException):
            self.service.redeem_stamps(db, store_id=store.id, card_id=card.id)
