"""Tests for the ReferralService business logic."""

from __future__ import annotations

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

import pytest

from referral.config import init_referral
from referral.models import (
    ReferralCode,
    ReferralConfig,
    ReferralStatus,
    RewardType,
)
from referral.service import ReferralService
from referral.store import InMemoryReferralStore


# ---------------------------------------------------------------------------
# Code creation
# ---------------------------------------------------------------------------


class TestCreateCode:
    async def test_creates_valid_code(self, service: ReferralService) -> None:
        code = await service.create_code("user-1")
        assert code.user_id == "user-1"
        assert len(code.code) == 8
        assert code.code.isalnum()
        assert code.uses == 0
        assert code.active is True

    async def test_creates_code_with_max_uses(self, service: ReferralService) -> None:
        code = await service.create_code("user-1", max_uses=5)
        assert code.max_uses == 5

    async def test_code_uses_default_max_from_config(self, service: ReferralService) -> None:
        code = await service.create_code("user-1")
        assert code.max_uses == 10  # from initialized_config fixture

    async def test_code_has_expiry(self, service: ReferralService) -> None:
        code = await service.create_code("user-1")
        assert code.expires_at is not None
        expected_delta = timedelta(days=90)
        actual_delta = code.expires_at - code.created_at
        assert abs(actual_delta - expected_delta) < timedelta(seconds=5)

    async def test_creates_unique_codes(self, service: ReferralService) -> None:
        codes = set()
        for _ in range(20):
            code = await service.create_code("user-1")
            codes.add(code.code)
        assert len(codes) == 20

    async def test_max_codes_per_user_limit(
        self,
        store: InMemoryReferralStore,
    ) -> None:
        config = ReferralConfig(max_referrals_per_user=3)
        init_referral(config)
        svc = ReferralService(store)

        for _ in range(3):
            await svc.create_code("user-limited")

        with pytest.raises(ValueError, match="maximum"):
            await svc.create_code("user-limited")

    async def test_inactive_codes_do_not_count_toward_limit(
        self,
        store: InMemoryReferralStore,
    ) -> None:
        config = ReferralConfig(max_referrals_per_user=2)
        init_referral(config)
        svc = ReferralService(store)

        c1 = await svc.create_code("user-limited")
        await svc.create_code("user-limited")
        # Deactivate one
        await svc.deactivate_code(c1.code)
        # Should allow a new one now
        c3 = await svc.create_code("user-limited")
        assert c3.active is True


# ---------------------------------------------------------------------------
# Code lookup
# ---------------------------------------------------------------------------


class TestCodeLookup:
    async def test_get_code(self, service: ReferralService) -> None:
        created = await service.create_code("user-1")
        found = await service.get_code(created.code)
        assert found is not None
        assert found.id == created.id

    async def test_get_code_not_found(self, service: ReferralService) -> None:
        result = await service.get_code("NONEXIST")
        assert result is None

    async def test_get_user_codes(self, service: ReferralService) -> None:
        await service.create_code("user-1")
        await service.create_code("user-1")
        await service.create_code("user-2")
        codes = await service.get_user_codes("user-1")
        assert len(codes) == 2

    async def test_get_user_codes_empty(self, service: ReferralService) -> None:
        codes = await service.get_user_codes("nobody")
        assert codes == []


# ---------------------------------------------------------------------------
# Code deactivation
# ---------------------------------------------------------------------------


class TestDeactivateCode:
    async def test_deactivate(self, service: ReferralService) -> None:
        created = await service.create_code("user-1")
        result = await service.deactivate_code(created.code)
        assert result is not None
        assert result.active is False

    async def test_deactivate_nonexistent(self, service: ReferralService) -> None:
        result = await service.deactivate_code("NOPE")
        assert result is None


# ---------------------------------------------------------------------------
# Apply referral
# ---------------------------------------------------------------------------


class TestApplyReferral:
    async def test_apply_valid_referral(self, service: ReferralService) -> None:
        code = await service.create_code("referrer")
        referral = await service.apply_referral(code.code, "new-user")
        assert referral.referrer_id == "referrer"
        assert referral.referred_id == "new-user"
        assert referral.code == code.code
        assert referral.status == ReferralStatus.PENDING

    async def test_apply_increments_usage(self, service: ReferralService) -> None:
        code = await service.create_code("referrer")
        await service.apply_referral(code.code, "new-user-1")
        updated = await service.get_code(code.code)
        assert updated is not None
        assert updated.uses == 1

    async def test_apply_nonexistent_code(self, service: ReferralService) -> None:
        with pytest.raises(ValueError, match="not found"):
            await service.apply_referral("BADCODE", "new-user")

    async def test_apply_inactive_code(self, service: ReferralService) -> None:
        code = await service.create_code("referrer")
        await service.deactivate_code(code.code)
        with pytest.raises(ValueError, match="no longer active"):
            await service.apply_referral(code.code, "new-user")

    async def test_apply_expired_code(
        self,
        store: InMemoryReferralStore,
    ) -> None:
        config = ReferralConfig(expiry_days=0)
        init_referral(config)
        svc = ReferralService(store)
        # Manually create an expired code
        expired_code = ReferralCode(
            id=uuid4(),
            code="EXPIRED1",
            user_id="referrer",
            uses=0,
            max_uses=10,
            created_at=datetime.now(timezone.utc) - timedelta(days=10),
            expires_at=datetime.now(timezone.utc) - timedelta(days=1),
            active=True,
        )
        await store.save_code(expired_code)
        with pytest.raises(ValueError, match="expired"):
            await svc.apply_referral("EXPIRED1", "new-user")

    async def test_apply_exhausted_code(
        self,
        store: InMemoryReferralStore,
    ) -> None:
        config = ReferralConfig(default_code_max_uses=1)
        init_referral(config)
        svc = ReferralService(store)
        code = await svc.create_code("referrer")
        await svc.apply_referral(code.code, "new-user-1")
        with pytest.raises(ValueError, match="maximum uses"):
            await svc.apply_referral(code.code, "new-user-2")

    async def test_apply_self_referral_rejected(self, service: ReferralService) -> None:
        code = await service.create_code("user-1")
        with pytest.raises(ValueError, match="themselves"):
            await service.apply_referral(code.code, "user-1")

    async def test_apply_duplicate_referral_rejected(self, service: ReferralService) -> None:
        code = await service.create_code("referrer")
        await service.apply_referral(code.code, "new-user")
        with pytest.raises(ValueError, match="already been referred"):
            await service.apply_referral(code.code, "new-user")


# ---------------------------------------------------------------------------
# Complete referral
# ---------------------------------------------------------------------------


class TestCompleteReferral:
    async def test_complete_pending_referral(self, service: ReferralService) -> None:
        code = await service.create_code("referrer")
        referral = await service.apply_referral(code.code, "new-user")
        completed = await service.complete_referral(referral.id)
        assert completed.status == ReferralStatus.COMPLETED
        assert completed.completed_at is not None

    async def test_complete_nonexistent_referral(self, service: ReferralService) -> None:
        with pytest.raises(ValueError, match="not found"):
            await service.complete_referral(uuid4())

    async def test_complete_already_completed(self, service: ReferralService) -> None:
        code = await service.create_code("referrer")
        referral = await service.apply_referral(code.code, "new-user")
        await service.complete_referral(referral.id)
        with pytest.raises(ValueError, match="cannot be completed"):
            await service.complete_referral(referral.id)

    async def test_complete_accepts_string_uuid(self, service: ReferralService) -> None:
        code = await service.create_code("referrer")
        referral = await service.apply_referral(code.code, "new-user")
        completed = await service.complete_referral(str(referral.id))
        assert completed.status == ReferralStatus.COMPLETED


# ---------------------------------------------------------------------------
# Stats and maintenance
# ---------------------------------------------------------------------------


class TestStatsAndMaintenance:
    async def test_get_stats_empty(self, service: ReferralService) -> None:
        stats = await service.get_stats("user-1")
        assert stats.total_referrals == 0

    async def test_get_stats_with_referrals(self, service: ReferralService) -> None:
        code = await service.create_code("referrer")
        await service.apply_referral(code.code, "u1")
        code2 = await service.create_code("referrer")
        ref2 = await service.apply_referral(code2.code, "u2")
        await service.complete_referral(ref2.id)

        stats = await service.get_stats("referrer")
        assert stats.total_referrals == 2
        assert stats.completed == 1
        assert stats.pending == 1

    async def test_expire_stale(
        self,
        store: InMemoryReferralStore,
    ) -> None:
        config = ReferralConfig(expiry_days=30)
        init_referral(config)
        svc = ReferralService(store)

        code = await svc.create_code("referrer")
        referral = await svc.apply_referral(code.code, "new-user")
        # Backdate the referral
        referral.created_at = datetime.now(timezone.utc) - timedelta(days=60)
        await store.update_referral(referral)

        expired_count = await svc.expire_stale()
        assert expired_count == 1

    async def test_expire_stale_skips_recent(self, service: ReferralService) -> None:
        code = await service.create_code("referrer")
        await service.apply_referral(code.code, "new-user")
        expired_count = await service.expire_stale()
        assert expired_count == 0

    async def test_get_leaderboard(self, service: ReferralService) -> None:
        # Create referrals for 2 users
        c1 = await service.create_code("u1")
        await service.apply_referral(c1.code, "r1")

        c2 = await service.create_code("u2")
        ref2 = await service.apply_referral(c2.code, "r2")
        await service.complete_referral(ref2.id)
        c3 = await service.create_code("u2")
        ref3 = await service.apply_referral(c3.code, "r3")
        await service.complete_referral(ref3.id)

        board = await service.get_leaderboard(limit=10)
        assert len(board) >= 1
        assert board[0].user_id == "u2"
