"""Tests for the in-memory referral store."""

from __future__ import annotations

from datetime import datetime, timedelta, timezone
from uuid import uuid4

import pytest

from referral.models import (
    Referral,
    ReferralCode,
    ReferralReward,
    ReferralStatus,
    RewardStatus,
    RewardType,
)
from referral.store import InMemoryReferralStore


@pytest.fixture()
def store() -> InMemoryReferralStore:
    return InMemoryReferralStore()


def _make_code(
    user_id: str = "user-1",
    code: str = "TESTCODE",
    **kwargs,
) -> ReferralCode:
    now = datetime.now(timezone.utc)
    defaults = dict(
        id=uuid4(),
        code=code,
        user_id=user_id,
        uses=0,
        max_uses=10,
        created_at=now,
        expires_at=now + timedelta(days=90),
        active=True,
    )
    defaults.update(kwargs)
    return ReferralCode(**defaults)


def _make_referral(
    referrer_id: str = "user-1",
    referred_id: str = "user-2",
    code: str = "TESTCODE",
    status: ReferralStatus = ReferralStatus.PENDING,
    **kwargs,
) -> Referral:
    now = datetime.now(timezone.utc)
    defaults = dict(
        id=uuid4(),
        referrer_id=referrer_id,
        referred_id=referred_id,
        code=code,
        status=status,
        created_at=now,
    )
    defaults.update(kwargs)
    return Referral(**defaults)


def _make_reward(
    user_id: str = "user-1",
    status: RewardStatus = RewardStatus.GRANTED,
    amount: float = 10.0,
    **kwargs,
) -> ReferralReward:
    defaults = dict(
        id=uuid4(),
        user_id=user_id,
        referral_id=uuid4(),
        reward_type=RewardType.CREDIT,
        amount=amount,
        status=status,
        granted_at=datetime.now(timezone.utc),
    )
    defaults.update(kwargs)
    return ReferralReward(**defaults)


# ---------------------------------------------------------------------------
# Code operations
# ---------------------------------------------------------------------------


class TestCodeOperations:
    async def test_save_and_get_code(self, store: InMemoryReferralStore) -> None:
        code = _make_code()
        await store.save_code(code)
        result = await store.get_code("TESTCODE")
        assert result is not None
        assert result.code == "TESTCODE"

    async def test_get_code_not_found(self, store: InMemoryReferralStore) -> None:
        result = await store.get_code("NONEXISTENT")
        assert result is None

    async def test_get_code_by_id(self, store: InMemoryReferralStore) -> None:
        code = _make_code()
        await store.save_code(code)
        result = await store.get_code_by_id(code.id)
        assert result is not None
        assert result.id == code.id

    async def test_get_code_by_id_not_found(self, store: InMemoryReferralStore) -> None:
        result = await store.get_code_by_id(uuid4())
        assert result is None

    async def test_get_user_codes(self, store: InMemoryReferralStore) -> None:
        await store.save_code(_make_code(code="CODE1"))
        await store.save_code(_make_code(code="CODE2"))
        await store.save_code(_make_code(user_id="other", code="CODE3"))
        result = await store.get_user_codes("user-1")
        assert len(result) == 2

    async def test_get_user_codes_empty(self, store: InMemoryReferralStore) -> None:
        result = await store.get_user_codes("nobody")
        assert result == []

    async def test_update_code(self, store: InMemoryReferralStore) -> None:
        code = _make_code()
        await store.save_code(code)
        code.uses = 5
        code.active = False
        await store.update_code(code)
        result = await store.get_code("TESTCODE")
        assert result is not None
        assert result.uses == 5
        assert result.active is False


# ---------------------------------------------------------------------------
# Referral operations
# ---------------------------------------------------------------------------


class TestReferralOperations:
    async def test_save_and_get_referral(self, store: InMemoryReferralStore) -> None:
        ref = _make_referral()
        await store.save_referral(ref)
        result = await store.get_referral(ref.id)
        assert result is not None
        assert result.referrer_id == "user-1"

    async def test_get_referral_not_found(self, store: InMemoryReferralStore) -> None:
        result = await store.get_referral(uuid4())
        assert result is None

    async def test_list_referrals_by_referrer(self, store: InMemoryReferralStore) -> None:
        await store.save_referral(_make_referral(referrer_id="u1", referred_id="u2"))
        await store.save_referral(_make_referral(referrer_id="u1", referred_id="u3"))
        await store.save_referral(_make_referral(referrer_id="u9", referred_id="u4"))
        result = await store.list_referrals(referrer_id="u1")
        assert len(result) == 2

    async def test_list_referrals_by_referred(self, store: InMemoryReferralStore) -> None:
        await store.save_referral(_make_referral(referrer_id="u1", referred_id="target"))
        await store.save_referral(_make_referral(referrer_id="u2", referred_id="other"))
        result = await store.list_referrals(referred_id="target")
        assert len(result) == 1

    async def test_list_referrals_by_status(self, store: InMemoryReferralStore) -> None:
        await store.save_referral(_make_referral(status=ReferralStatus.PENDING))
        await store.save_referral(
            _make_referral(
                referred_id="u3", status=ReferralStatus.COMPLETED
            )
        )
        result = await store.list_referrals(status=ReferralStatus.PENDING)
        assert len(result) == 1

    async def test_list_referrals_pagination(self, store: InMemoryReferralStore) -> None:
        for i in range(5):
            await store.save_referral(
                _make_referral(referred_id=f"user-{i}", referrer_id="u1")
            )
        page1 = await store.list_referrals(referrer_id="u1", limit=2, offset=0)
        page2 = await store.list_referrals(referrer_id="u1", limit=2, offset=2)
        page3 = await store.list_referrals(referrer_id="u1", limit=2, offset=4)
        assert len(page1) == 2
        assert len(page2) == 2
        assert len(page3) == 1

    async def test_update_referral(self, store: InMemoryReferralStore) -> None:
        ref = _make_referral()
        await store.save_referral(ref)
        ref.status = ReferralStatus.COMPLETED
        ref.completed_at = datetime.now(timezone.utc)
        await store.update_referral(ref)
        result = await store.get_referral(ref.id)
        assert result is not None
        assert result.status == ReferralStatus.COMPLETED

    async def test_count_user_referrals(self, store: InMemoryReferralStore) -> None:
        await store.save_referral(_make_referral(referrer_id="u1", referred_id="u2"))
        await store.save_referral(_make_referral(referrer_id="u1", referred_id="u3"))
        await store.save_referral(_make_referral(referrer_id="u9", referred_id="u4"))
        assert await store.count_user_referrals("u1") == 2
        assert await store.count_user_referrals("u9") == 1
        assert await store.count_user_referrals("nobody") == 0


# ---------------------------------------------------------------------------
# Reward operations
# ---------------------------------------------------------------------------


class TestRewardOperations:
    async def test_save_and_get_rewards(self, store: InMemoryReferralStore) -> None:
        reward = _make_reward()
        await store.save_reward(reward)
        result = await store.get_user_rewards("user-1")
        assert len(result) == 1
        assert result[0].amount == 10.0

    async def test_get_user_rewards_empty(self, store: InMemoryReferralStore) -> None:
        result = await store.get_user_rewards("nobody")
        assert result == []

    async def test_get_pending_rewards(self, store: InMemoryReferralStore) -> None:
        await store.save_reward(_make_reward(status=RewardStatus.PENDING))
        await store.save_reward(_make_reward(status=RewardStatus.GRANTED))
        await store.save_reward(_make_reward(status=RewardStatus.PENDING))
        result = await store.get_pending_rewards()
        assert len(result) == 2

    async def test_update_reward(self, store: InMemoryReferralStore) -> None:
        reward = _make_reward(status=RewardStatus.PENDING)
        await store.save_reward(reward)
        reward.status = RewardStatus.GRANTED
        reward.granted_at = datetime.now(timezone.utc)
        await store.update_reward(reward)
        rewards = await store.get_user_rewards("user-1")
        assert rewards[0].status == RewardStatus.GRANTED


# ---------------------------------------------------------------------------
# Stats operations
# ---------------------------------------------------------------------------


class TestStatsOperations:
    async def test_empty_stats(self, store: InMemoryReferralStore) -> None:
        stats = await store.get_stats("user-1")
        assert stats.total_referrals == 0
        assert stats.completed == 0
        assert stats.pending == 0
        assert stats.expired == 0
        assert stats.total_rewards_earned == 0.0

    async def test_stats_with_data(self, store: InMemoryReferralStore) -> None:
        await store.save_referral(
            _make_referral(referrer_id="u1", referred_id="u2", status=ReferralStatus.COMPLETED)
        )
        await store.save_referral(
            _make_referral(referrer_id="u1", referred_id="u3", status=ReferralStatus.PENDING)
        )
        await store.save_referral(
            _make_referral(referrer_id="u1", referred_id="u4", status=ReferralStatus.EXPIRED)
        )
        await store.save_reward(_make_reward(user_id="u1", status=RewardStatus.GRANTED, amount=10))
        await store.save_reward(_make_reward(user_id="u1", status=RewardStatus.GRANTED, amount=20))
        await store.save_reward(
            _make_reward(user_id="u1", status=RewardStatus.PENDING, amount=999)
        )

        stats = await store.get_stats("u1")
        assert stats.total_referrals == 3
        assert stats.completed == 1
        assert stats.pending == 1
        assert stats.expired == 1
        assert stats.total_rewards_earned == 30.0

    async def test_stats_rewarded_counts_as_completed(
        self, store: InMemoryReferralStore
    ) -> None:
        await store.save_referral(
            _make_referral(referrer_id="u1", referred_id="u2", status=ReferralStatus.REWARDED)
        )
        stats = await store.get_stats("u1")
        assert stats.completed == 1

    async def test_leaderboard(self, store: InMemoryReferralStore) -> None:
        # user-1 has 2 completed
        for i in range(2):
            await store.save_referral(
                _make_referral(
                    referrer_id="user-1",
                    referred_id=f"ref-{i}",
                    status=ReferralStatus.COMPLETED,
                )
            )
        # user-2 has 3 completed
        for i in range(3):
            await store.save_referral(
                _make_referral(
                    referrer_id="user-2",
                    referred_id=f"ref-{10 + i}",
                    status=ReferralStatus.COMPLETED,
                )
            )

        board = await store.get_leaderboard(limit=10)
        assert len(board) == 2
        assert board[0].user_id == "user-2"
        assert board[0].completed == 3
        assert board[1].user_id == "user-1"

    async def test_leaderboard_limit(self, store: InMemoryReferralStore) -> None:
        for i in range(5):
            await store.save_referral(
                _make_referral(
                    referrer_id=f"user-{i}",
                    referred_id=f"ref-{i}",
                    status=ReferralStatus.COMPLETED,
                )
            )
        board = await store.get_leaderboard(limit=2)
        assert len(board) == 2


# ---------------------------------------------------------------------------
# Maintenance operations
# ---------------------------------------------------------------------------


class TestMaintenanceOperations:
    async def test_expire_stale(self, store: InMemoryReferralStore) -> None:
        old = datetime.now(timezone.utc) - timedelta(days=100)
        recent = datetime.now(timezone.utc) - timedelta(days=1)

        await store.save_referral(
            _make_referral(referrer_id="u1", referred_id="u2", created_at=old)
        )
        await store.save_referral(
            _make_referral(referrer_id="u1", referred_id="u3", created_at=recent)
        )
        await store.save_referral(
            _make_referral(
                referrer_id="u1",
                referred_id="u4",
                created_at=old,
                status=ReferralStatus.COMPLETED,
            )
        )

        cutoff = datetime.now(timezone.utc) - timedelta(days=90)
        expired_count = await store.expire_stale(cutoff)
        assert expired_count == 1

    async def test_expire_stale_none_eligible(self, store: InMemoryReferralStore) -> None:
        recent = datetime.now(timezone.utc)
        await store.save_referral(
            _make_referral(referrer_id="u1", referred_id="u2", created_at=recent)
        )
        cutoff = datetime.now(timezone.utc) - timedelta(days=90)
        assert await store.expire_stale(cutoff) == 0
