"""Tests for the RewardEngine."""

from __future__ import annotations

from datetime import datetime, timezone
from unittest.mock import AsyncMock
from uuid import uuid4

import pytest

from referral.config import init_referral
from referral.models import (
    Referral,
    ReferralConfig,
    ReferralReward,
    ReferralStatus,
    RewardStatus,
    RewardType,
)
from referral.rewards import RewardEngine
from referral.store import InMemoryReferralStore


def _make_referral(
    referrer_id: str = "referrer",
    referred_id: str = "referred",
    status: ReferralStatus = ReferralStatus.COMPLETED,
) -> Referral:
    return Referral(
        id=uuid4(),
        referrer_id=referrer_id,
        referred_id=referred_id,
        code="TESTCODE",
        status=status,
        created_at=datetime.now(timezone.utc),
        completed_at=datetime.now(timezone.utc) if status == ReferralStatus.COMPLETED else None,
    )


# ---------------------------------------------------------------------------
# Grant reward
# ---------------------------------------------------------------------------


class TestGrantReward:
    async def test_grant_reward_uses_config_defaults(
        self,
        reward_engine: RewardEngine,
    ) -> None:
        referral = _make_referral()
        reward = await reward_engine.grant_reward(referral)
        assert reward.user_id == "referrer"
        assert reward.reward_type == RewardType.CREDIT
        assert reward.amount == 10.0
        assert reward.status == RewardStatus.GRANTED
        assert reward.granted_at is not None

    async def test_grant_reward_custom_type_and_amount(
        self,
        reward_engine: RewardEngine,
    ) -> None:
        referral = _make_referral()
        reward = await reward_engine.grant_reward(
            referral, reward_type=RewardType.DISCOUNT, amount=25.0
        )
        assert reward.reward_type == RewardType.DISCOUNT
        assert reward.amount == 25.0

    async def test_grant_reward_custom_user(
        self,
        reward_engine: RewardEngine,
    ) -> None:
        referral = _make_referral()
        reward = await reward_engine.grant_reward(referral, user_id="custom-user")
        assert reward.user_id == "custom-user"

    async def test_grant_reward_persisted_in_store(
        self,
        store: InMemoryReferralStore,
        reward_engine: RewardEngine,
    ) -> None:
        referral = _make_referral()
        await reward_engine.grant_reward(referral)
        rewards = await store.get_user_rewards("referrer")
        assert len(rewards) == 1

    async def test_grant_reward_zero_amount(
        self,
        reward_engine: RewardEngine,
    ) -> None:
        referral = _make_referral()
        reward = await reward_engine.grant_reward(referral, amount=0)
        assert reward.amount == 0

    async def test_grant_reward_invokes_callback(
        self,
        store: InMemoryReferralStore,
        initialized_config: ReferralConfig,
    ) -> None:
        callback = AsyncMock()
        engine = RewardEngine(store, on_reward_granted=callback)
        referral = _make_referral()
        reward = await engine.grant_reward(referral)
        callback.assert_awaited_once_with(reward)


# ---------------------------------------------------------------------------
# Double-sided rewards
# ---------------------------------------------------------------------------


class TestDoubleSidedRewards:
    async def test_grant_double_sided(
        self,
        reward_engine: RewardEngine,
    ) -> None:
        referral = _make_referral(referrer_id="ref1", referred_id="ref2")
        r1, r2 = await reward_engine.grant_double_sided(referral)
        assert r1.user_id == "ref1"
        assert r2.user_id == "ref2"
        assert r1.amount == r2.amount == 10.0

    async def test_grant_double_sided_custom_amount(
        self,
        reward_engine: RewardEngine,
    ) -> None:
        referral = _make_referral()
        r1, r2 = await reward_engine.grant_double_sided(referral, amount=50.0)
        assert r1.amount == 50.0
        assert r2.amount == 50.0

    async def test_grant_double_sided_custom_type(
        self,
        reward_engine: RewardEngine,
    ) -> None:
        referral = _make_referral()
        r1, r2 = await reward_engine.grant_double_sided(
            referral, reward_type=RewardType.FEATURE
        )
        assert r1.reward_type == RewardType.FEATURE
        assert r2.reward_type == RewardType.FEATURE

    async def test_double_sided_creates_two_store_entries(
        self,
        store: InMemoryReferralStore,
        reward_engine: RewardEngine,
    ) -> None:
        referral = _make_referral(referrer_id="u1", referred_id="u2")
        await reward_engine.grant_double_sided(referral)
        assert len(await store.get_user_rewards("u1")) == 1
        assert len(await store.get_user_rewards("u2")) == 1


# ---------------------------------------------------------------------------
# Process completed referrals
# ---------------------------------------------------------------------------


class TestProcessCompletedReferrals:
    async def test_process_completed_double_sided(
        self,
        reward_engine: RewardEngine,
    ) -> None:
        referral = _make_referral()
        rewards = await reward_engine.process_completed_referrals([referral])
        assert len(rewards) == 2  # double-sided
        assert referral.status == ReferralStatus.REWARDED

    async def test_process_completed_single_sided(
        self,
        store: InMemoryReferralStore,
        single_sided_config: ReferralConfig,
    ) -> None:
        init_referral(single_sided_config)
        engine = RewardEngine(store)
        referral = _make_referral()
        rewards = await engine.process_completed_referrals([referral])
        assert len(rewards) == 1
        assert rewards[0].user_id == "referrer"
        assert referral.status == ReferralStatus.REWARDED

    async def test_skips_non_completed_referrals(
        self,
        reward_engine: RewardEngine,
    ) -> None:
        pending = _make_referral(status=ReferralStatus.PENDING)
        expired = _make_referral(status=ReferralStatus.EXPIRED)
        rewards = await reward_engine.process_completed_referrals([pending, expired])
        assert len(rewards) == 0

    async def test_process_batch(
        self,
        reward_engine: RewardEngine,
    ) -> None:
        refs = [_make_referral(referred_id=f"u{i}") for i in range(3)]
        rewards = await reward_engine.process_completed_referrals(refs)
        assert len(rewards) == 6  # 3 referrals x 2 (double-sided)
        assert all(r.status == ReferralStatus.REWARDED for r in refs)

    async def test_process_mixed_statuses(
        self,
        reward_engine: RewardEngine,
    ) -> None:
        completed = _make_referral(referred_id="c1")
        pending = _make_referral(referred_id="p1", status=ReferralStatus.PENDING)
        rewards = await reward_engine.process_completed_referrals([completed, pending])
        assert len(rewards) == 2
        assert completed.status == ReferralStatus.REWARDED
        assert pending.status == ReferralStatus.PENDING


# ---------------------------------------------------------------------------
# Process pending rewards
# ---------------------------------------------------------------------------


class TestProcessPendingRewards:
    async def test_process_pending(
        self,
        store: InMemoryReferralStore,
        reward_engine: RewardEngine,
    ) -> None:
        pending1 = ReferralReward(
            id=uuid4(),
            user_id="u1",
            referral_id=uuid4(),
            reward_type=RewardType.CREDIT,
            amount=10.0,
            status=RewardStatus.PENDING,
        )
        pending2 = ReferralReward(
            id=uuid4(),
            user_id="u2",
            referral_id=uuid4(),
            reward_type=RewardType.CREDIT,
            amount=5.0,
            status=RewardStatus.PENDING,
        )
        already_granted = ReferralReward(
            id=uuid4(),
            user_id="u3",
            referral_id=uuid4(),
            reward_type=RewardType.CREDIT,
            amount=20.0,
            status=RewardStatus.GRANTED,
            granted_at=datetime.now(timezone.utc),
        )
        await store.save_reward(pending1)
        await store.save_reward(pending2)
        await store.save_reward(already_granted)

        processed = await reward_engine.process_pending_rewards()
        assert len(processed) == 2
        assert all(r.status == RewardStatus.GRANTED for r in processed)
        assert all(r.granted_at is not None for r in processed)

    async def test_process_pending_invokes_callback(
        self,
        store: InMemoryReferralStore,
        initialized_config: ReferralConfig,
    ) -> None:
        callback = AsyncMock()
        engine = RewardEngine(store, on_reward_granted=callback)
        pending = ReferralReward(
            id=uuid4(),
            user_id="u1",
            referral_id=uuid4(),
            reward_type=RewardType.CREDIT,
            amount=10.0,
            status=RewardStatus.PENDING,
        )
        await store.save_reward(pending)
        await engine.process_pending_rewards()
        callback.assert_awaited_once()

    async def test_process_pending_empty(
        self,
        reward_engine: RewardEngine,
    ) -> None:
        processed = await reward_engine.process_pending_rewards()
        assert processed == []
