import datetime
import random
from decimal import Decimal

import post_office.models
import pytz
from dateutil.parser import parse as parse_date
from django.contrib.admin import ACTION_CHECKBOX_NAME
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.test import TestCase
from django.test import TransactionTestCase
from django.urls import reverse

from tracker import models, prizeutil, randgen
from .util import today_noon, MigrationsTestCase


class TestPrizeGameRange(TransactionTestCase):
    def setUp(self):
        self.rand = random.Random(None)
        self.event = randgen.generate_event(self.rand, start_time=today_noon)
        self.event.save()

    def test_prize_range_single(self):
        runs = randgen.generate_runs(self.rand, self.event, 4, scheduled=True)
        run = runs[1]
        prize = randgen.generate_prize(
            self.rand, event=self.event, start_run=run, end_run=run
        )
        prizeRuns = prize.games_range()
        self.assertEqual(1, prizeRuns.count())
        self.assertEqual(run.id, prizeRuns[0].id)

    def test_prize_range_pair(self):
        runs = randgen.generate_runs(self.rand, self.event, 5, scheduled=True)
        startRun = runs[2]
        endRun = runs[3]
        prize = randgen.generate_prize(
            self.rand, event=self.event, start_run=startRun, end_run=endRun
        )
        prizeRuns = prize.games_range()
        self.assertEqual(2, prizeRuns.count())
        self.assertEqual(startRun.id, prizeRuns[0].id)
        self.assertEqual(endRun.id, prizeRuns[1].id)

    def test_prize_range_gap(self):
        runs = randgen.generate_runs(self.rand, self.event, 7, scheduled=True)
        runsSlice = runs[2:5]
        prize = randgen.generate_prize(
            self.rand, event=self.event, start_run=runsSlice[0], end_run=runsSlice[-1]
        )
        prizeRuns = prize.games_range()
        self.assertEqual(len(runsSlice), prizeRuns.count())
        for i in range(0, len(runsSlice)):
            self.assertEqual(runsSlice[i].id, prizeRuns[i].id)

    def test_time_prize_no_range(self):
        runs = randgen.generate_runs(self.rand, self.event, 7, scheduled=True)
        eventEnd = runs[-1].endtime
        timeA = randgen.random_time(self.rand, self.event.datetime, eventEnd)
        timeB = randgen.random_time(self.rand, self.event.datetime, eventEnd)
        randomStart = min(timeA, timeB)
        randomEnd = max(timeA, timeB)
        prize = randgen.generate_prize(
            self.rand, event=self.event, start_time=randomStart, end_time=randomEnd
        )
        prizeRuns = prize.games_range()
        self.assertEqual(0, prizeRuns.count())
        self.assertEqual(randomStart, prize.start_draw_time())
        self.assertEqual(randomEnd, prize.end_draw_time())


class TestPrizeDrawingGeneratedEvent(TransactionTestCase):
    def setUp(self):
        self.eventStart = parse_date('2014-01-01 16:00:00Z')
        self.rand = random.Random(516273)
        self.event = randgen.build_random_event(
            self.rand, start_time=self.eventStart, num_donors=100, num_runs=50
        )
        self.runsList = list(models.SpeedRun.objects.filter(event=self.event))
        self.donorList = list(models.Donor.objects.all())

    def test_draw_random_prize_no_donations(self):
        prizeList = randgen.generate_prizes(
            self.rand, self.event, 50, list_of_runs=self.runsList
        )
        for prize in prizeList:
            for randomness in [True, False]:
                for useSum in [True, False]:
                    prize.randomdraw = randomness
                    prize.sumdonations = useSum
                    prize.save()
                    eligibleDonors = prize.eligible_donors()
                    self.assertEqual(0, len(eligibleDonors))
                    result, message = prizeutil.draw_prize(prize)
                    self.assertFalse(result)
                    self.assertEqual(0, prize.current_win_count())

    def test_draw_prize_one_donor(self):
        startRun = self.runsList[14]
        endRun = self.runsList[28]
        for useRandom in [True, False]:
            for useSum in [True, False]:
                for donationSize in ['top', 'bottom', 'above', 'below', 'within']:
                    prize = randgen.generate_prize(
                        self.rand,
                        event=self.event,
                        sum_donations=useSum,
                        random_draw=useRandom,
                        start_run=startRun,
                        end_run=endRun,
                    )
                    prize.save()
                    donor = randgen.pick_random_element(self.rand, self.donorList)
                    donation = randgen.generate_donation(
                        self.rand,
                        donor=donor,
                        event=self.event,
                        min_time=prize.start_draw_time(),
                        max_time=prize.end_draw_time(),
                    )
                    if donationSize == 'above':
                        donation.amount = prize.maximumbid + Decimal('5.00')
                    elif donationSize == 'top':
                        donation.amount = prize.maximumbid
                    elif donationSize == 'within':
                        donation.amount = randgen.random_amount(
                            self.rand,
                            min_amount=prize.minimumbid,
                            max_amount=prize.maximumbid,
                        )
                    elif donationSize == 'bottom':
                        donation.amount = prize.minimumbid
                    elif donationSize == 'below':
                        donation.amount = max(
                            Decimal('0.00'), prize.minimumbid - Decimal('5.00')
                        )
                    donation.save()
                    eligibleDonors = prize.eligible_donors()
                    if donationSize == 'below' and prize.randomdraw:
                        self.assertEqual(0, len(eligibleDonors))
                    else:
                        self.assertEqual(1, len(eligibleDonors))
                        self.assertEqual(donor.id, eligibleDonors[0]['donor'])
                        self.assertEqual(donation.amount, eligibleDonors[0]['amount'])
                        if prize.sumdonations and prize.randomdraw:
                            if donationSize == 'top' or donationSize == 'above':
                                expectedRatio = float(
                                    prize.maximumbid / prize.minimumbid
                                )
                            else:
                                expectedRatio = float(
                                    donation.amount / prize.minimumbid
                                )
                            self.assertAlmostEqual(
                                expectedRatio, eligibleDonors[0]['weight']
                            )
                        else:
                            self.assertEqual(1.0, eligibleDonors[0]['weight'])
                    result, message = prizeutil.draw_prize(prize)
                    if donationSize != 'below' or not prize.randomdraw:
                        self.assertTrue(result)
                        self.assertEqual(donor, prize.get_winner())
                    else:
                        self.assertFalse(result)
                        self.assertEqual(None, prize.get_winner())
                    donation.delete()
                    prize.prizewinner_set.all().delete()
                    prize.delete()

    def test_draw_prize_multiple_donors_random_nosum(self):
        startRun = self.runsList[28]
        endRun = self.runsList[30]
        prize = randgen.generate_prize(
            self.rand,
            event=self.event,
            sum_donations=False,
            random_draw=True,
            start_run=startRun,
            end_run=endRun,
        )
        prize.save()
        donationDonors = {}
        for donor in self.donorList:
            if self.rand.getrandbits(1) == 0:
                donation = randgen.generate_donation(
                    self.rand,
                    donor=donor,
                    event=self.event,
                    min_amount=prize.minimumbid,
                    max_amount=prize.minimumbid + Decimal('100.00'),
                    min_time=prize.start_draw_time(),
                    max_time=prize.end_draw_time(),
                )
                donation.save()
                donationDonors[donor.id] = donor
            # Add a few red herrings to make sure out of range donations aren't
            # used
            donation2 = randgen.generate_donation(
                self.rand,
                donor=donor,
                event=self.event,
                min_amount=prize.minimumbid,
                max_amount=prize.minimumbid + Decimal('100.00'),
                max_time=prize.start_draw_time() - datetime.timedelta(seconds=1),
            )
            donation2.save()
            donation3 = randgen.generate_donation(
                self.rand,
                donor=donor,
                event=self.event,
                min_amount=prize.minimumbid,
                max_amount=prize.minimumbid + Decimal('100.00'),
                min_time=prize.end_draw_time() + datetime.timedelta(seconds=1),
            )
            donation3.save()
        eligibleDonors = prize.eligible_donors()
        self.assertEqual(len(list(donationDonors.keys())), len(eligibleDonors))
        for eligibleDonor in eligibleDonors:
            found = False
            if eligibleDonor['donor'] in donationDonors:
                donor = donationDonors[eligibleDonor['donor']]
                donation = donor.donation_set.filter(
                    timereceived__gte=prize.start_draw_time(),
                    timereceived__lte=prize.end_draw_time(),
                )[0]
                self.assertEqual(donation.amount, eligibleDonor['amount'])
                self.assertEqual(1.0, eligibleDonor['weight'])
                found = True
            self.assertTrue(found and 'Could not find the donor in the list')
        winners = []
        for seed in [15634, 12512, 666]:
            result, message = prizeutil.draw_prize(prize, seed)
            self.assertTrue(result)
            self.assertIn(prize.get_winner().id, donationDonors)
            winners.append(prize.get_winner())
            current = prize.get_winner()
            prize.prizewinner_set.all().delete()
            prize.save()
            result, message = prizeutil.draw_prize(prize, seed)
            self.assertTrue(result)
            self.assertEqual(current, prize.get_winner())
            prize.prizewinner_set.all().delete()
            prize.save()
        self.assertNotEqual(winners[0], winners[1])
        self.assertNotEqual(winners[1], winners[2])
        self.assertNotEqual(winners[0], winners[2])

    def test_draw_prize_multiple_donors_random_sum(self):
        startRun = self.runsList[41]
        endRun = self.runsList[46]
        prize = randgen.generate_prize(
            self.rand,
            event=self.event,
            sum_donations=True,
            random_draw=True,
            start_run=startRun,
            end_run=endRun,
        )
        prize.save()
        donationDonors = {}
        for donor in self.donorList:
            numDonations = self.rand.getrandbits(4)
            redHerrings = self.rand.getrandbits(4)
            donationDonors[donor.id] = {'donor': donor, 'amount': Decimal('0.00')}
            for i in range(0, numDonations):
                donation = randgen.generate_donation(
                    self.rand,
                    donor=donor,
                    event=self.event,
                    min_amount=Decimal('0.01'),
                    max_amount=prize.minimumbid - Decimal('0.10'),
                    min_time=prize.start_draw_time(),
                    max_time=prize.end_draw_time(),
                )
                donation.save()
                donationDonors[donor.id]['amount'] += donation.amount
            # toss in a few extras to keep the drawer on its toes
            for i in range(0, redHerrings):
                donation = None
                if self.rand.getrandbits(1) == 0:
                    donation = randgen.generate_donation(
                        self.rand,
                        donor=donor,
                        event=self.event,
                        min_amount=Decimal('0.01'),
                        max_amount=prize.minimumbid - Decimal('0.10'),
                        max_time=prize.start_draw_time()
                        - datetime.timedelta(seconds=1),
                    )
                else:
                    donation = randgen.generate_donation(
                        self.rand,
                        donor=donor,
                        event=self.event,
                        min_amount=Decimal('0.01'),
                        max_amount=prize.minimumbid - Decimal('0.10'),
                        min_time=prize.end_draw_time() + datetime.timedelta(seconds=1),
                    )
                donation.save()
            if donationDonors[donor.id]['amount'] < prize.minimumbid:
                del donationDonors[donor.id]
        eligibleDonors = prize.eligible_donors()
        self.assertEqual(len(list(donationDonors.keys())), len(eligibleDonors))
        found = False
        for eligibleDonor in eligibleDonors:
            if eligibleDonor['donor'] in donationDonors:
                entry = donationDonors[eligibleDonor['donor']]
                donor = entry['donor']
                if entry['amount'] >= prize.minimumbid:
                    donations = donor.donation_set.filter(
                        timereceived__gte=prize.start_draw_time(),
                        timereceived__lte=prize.end_draw_time(),
                    )
                    countAmount = Decimal('0.00')
                    for donation in donations:
                        countAmount += donation.amount
                    self.assertEqual(entry['amount'], eligibleDonor['amount'])
                    self.assertEqual(countAmount, eligibleDonor['amount'])
                    self.assertAlmostEqual(
                        min(
                            prize.maximumbid / prize.minimumbid,
                            entry['amount'] / prize.minimumbid,
                        ),
                        Decimal(eligibleDonor['weight']),
                    )
                    found = True
        # FIXME: what is this actually asserting? it's not very clear to me by glancing at it
        self.assertTrue(found, 'Could not find the donor in the list')
        winners = []
        for seed in [51234, 235426, 62363245]:
            result, message = prizeutil.draw_prize(prize, seed)
            self.assertTrue(result)
            self.assertIn(prize.get_winner().id, donationDonors)
            winners.append(prize.get_winner())
            current = prize.get_winner()
            prize.prizewinner_set.all().delete()
            prize.save()
            result, message = prizeutil.draw_prize(prize, seed)
            self.assertTrue(result)
            self.assertEqual(current, prize.get_winner())
            prize.prizewinner_set.all().delete()
            prize.save()
        self.assertNotEqual(winners[0], winners[1])
        self.assertNotEqual(winners[1], winners[2])
        self.assertNotEqual(winners[0], winners[2])

    def test_draw_prize_multiple_donors_norandom_nosum(self):
        startRun = self.runsList[25]
        endRun = self.runsList[34]
        prize = randgen.generate_prize(
            self.rand,
            event=self.event,
            sum_donations=False,
            random_draw=False,
            start_run=startRun,
            end_run=endRun,
        )
        prize.save()
        largestDonor = None
        largestAmount = Decimal('0.00')
        for donor in self.donorList:
            numDonations = self.rand.getrandbits(4)
            redHerrings = self.rand.getrandbits(4)
            for i in range(0, numDonations):
                donation = randgen.generate_donation(
                    self.rand,
                    donor=donor,
                    event=self.event,
                    min_amount=Decimal('0.01'),
                    max_amount=Decimal('1000.00'),
                    min_time=prize.start_draw_time(),
                    max_time=prize.end_draw_time(),
                )
                donation.save()
                if donation.amount > largestAmount:
                    largestDonor = donor
                    largestAmount = donation.amount
            # toss in a few extras to keep the drawer on its toes
            for i in range(0, redHerrings):
                donation = None
                if self.rand.getrandbits(1) == 0:
                    donation = randgen.generate_donation(
                        self.rand,
                        donor=donor,
                        event=self.event,
                        min_amount=Decimal('1000.01'),
                        max_amount=Decimal('2000.00'),
                        max_time=prize.start_draw_time()
                        - datetime.timedelta(seconds=1),
                    )
                else:
                    donation = randgen.generate_donation(
                        self.rand,
                        donor=donor,
                        event=self.event,
                        min_amount=Decimal('1000.01'),
                        max_amount=max(
                            Decimal('1000.01'), prize.minimumbid - Decimal('2000.00')
                        ),
                        min_time=prize.end_draw_time() + datetime.timedelta(seconds=1),
                    )
                donation.save()
        eligibleDonors = prize.eligible_donors()
        self.assertEqual(1, len(eligibleDonors))
        self.assertEqual(largestDonor.id, eligibleDonors[0]['donor'])
        self.assertEqual(1.0, eligibleDonors[0]['weight'])
        self.assertEqual(largestAmount, eligibleDonors[0]['amount'])
        for seed in [9524, 373, 747]:
            prize.prizewinner_set.all().delete()
            prize.save()
            result, message = prizeutil.draw_prize(prize, seed)
            self.assertTrue(result)
            self.assertEqual(largestDonor.id, prize.get_winner().id)
        newDonor = randgen.generate_donor(self.rand)
        newDonor.save()
        newDonation = randgen.generate_donation(
            self.rand,
            donor=newDonor,
            event=self.event,
            min_amount=Decimal('1000.01'),
            max_amount=Decimal('2000.00'),
            min_time=prize.start_draw_time(),
            max_time=prize.end_draw_time(),
        )
        newDonation.save()
        eligibleDonors = prize.eligible_donors()
        self.assertEqual(1, len(eligibleDonors))
        self.assertEqual(newDonor.id, eligibleDonors[0]['donor'])
        self.assertEqual(1.0, eligibleDonors[0]['weight'])
        self.assertEqual(newDonation.amount, eligibleDonors[0]['amount'])
        for seed in [9524, 373, 747]:
            prize.prizewinner_set.all().delete()
            prize.save()
            result, message = prizeutil.draw_prize(prize, seed)
            self.assertTrue(result)
            self.assertEqual(newDonor.id, prize.get_winner().id)

    def test_draw_prize_multiple_donors_norandom_sum(self):
        startRun = self.runsList[5]
        endRun = self.runsList[9]
        prize = randgen.generate_prize(
            self.rand,
            event=self.event,
            sum_donations=True,
            random_draw=False,
            start_run=startRun,
            end_run=endRun,
        )
        prize.save()
        donationDonors = {}
        for donor in self.donorList:
            numDonations = self.rand.getrandbits(4)
            redHerrings = self.rand.getrandbits(4)
            donationDonors[donor.id] = {'donor': donor, 'amount': Decimal('0.00')}
            for i in range(0, numDonations):
                donation = randgen.generate_donation(
                    self.rand,
                    donor=donor,
                    event=self.event,
                    min_amount=Decimal('0.01'),
                    max_amount=Decimal('100.00'),
                    min_time=prize.start_draw_time(),
                    max_time=prize.end_draw_time(),
                )
                donation.save()
                donationDonors[donor.id]['amount'] += donation.amount
            # toss in a few extras to keep the drawer on its toes
            for i in range(0, redHerrings):
                donation = None
                if self.rand.getrandbits(1) == 0:
                    donation = randgen.generate_donation(
                        self.rand,
                        donor=donor,
                        event=self.event,
                        min_amount=Decimal('1000.01'),
                        max_amount=Decimal('2000.00'),
                        max_time=prize.start_draw_time()
                        - datetime.timedelta(seconds=1),
                    )
                else:
                    donation = randgen.generate_donation(
                        self.rand,
                        donor=donor,
                        event=self.event,
                        min_amount=Decimal('1000.01'),
                        max_amount=max(
                            Decimal('1000.01'), prize.minimumbid - Decimal('2000.00')
                        ),
                        min_time=prize.end_draw_time() + datetime.timedelta(seconds=1),
                    )
                donation.save()
        maxDonor = max(list(donationDonors.items()), key=lambda x: x[1]['amount'])[1]
        eligibleDonors = prize.eligible_donors()
        self.assertEqual(1, len(eligibleDonors))
        self.assertEqual(maxDonor['donor'].id, eligibleDonors[0]['donor'])
        self.assertEqual(1.0, eligibleDonors[0]['weight'])
        self.assertEqual(maxDonor['amount'], eligibleDonors[0]['amount'])
        for seed in [9524, 373, 747]:
            prize.prizewinner_set.all().delete()
            prize.save()
            result, message = prizeutil.draw_prize(prize, seed)
            self.assertTrue(result)
            self.assertEqual(maxDonor['donor'].id, prize.get_winner().id)
        oldMaxDonor = maxDonor
        del donationDonors[oldMaxDonor['donor'].id]
        maxDonor = max(list(donationDonors.items()), key=lambda x: x[1]['amount'])[1]
        diff = oldMaxDonor['amount'] - maxDonor['amount']
        newDonor = maxDonor['donor']
        newDonation = randgen.generate_donation(
            self.rand,
            donor=newDonor,
            event=self.event,
            min_amount=diff + Decimal('0.01'),
            max_amount=diff + Decimal('100.00'),
            min_time=prize.start_draw_time(),
            max_time=prize.end_draw_time(),
        )
        newDonation.save()
        maxDonor['amount'] += newDonation.amount
        prize = models.Prize.objects.get(id=prize.id)
        eligibleDonors = prize.eligible_donors()
        self.assertEqual(1, len(eligibleDonors))
        self.assertEqual(maxDonor['donor'].id, eligibleDonors[0]['donor'])
        self.assertEqual(1.0, eligibleDonors[0]['weight'])
        self.assertEqual(maxDonor['amount'], eligibleDonors[0]['amount'])
        for seed in [9524, 373, 747]:
            prize.prizewinner_set.all().delete()
            prize.save()
            result, message = prizeutil.draw_prize(prize, seed)
            self.assertTrue(result)
            self.assertEqual(maxDonor['donor'].id, prize.get_winner().id)


class TestDonorPrizeEntryDraw(TransactionTestCase):
    def setUp(self):
        self.rand = random.Random(9239234)
        self.event = randgen.generate_event(self.rand)
        self.event.save()

    def testSingleEntry(self):
        donor = randgen.generate_donor(self.rand)
        donor.save()
        prize = randgen.generate_prize(self.rand, event=self.event)
        prize.save()
        entry = models.DonorPrizeEntry(donor=donor, prize=prize)
        entry.save()
        eligible = prize.eligible_donors()
        self.assertEqual(1, len(eligible))
        self.assertEqual(donor.pk, eligible[0]['donor'])
        self.assertEqual(entry.weight, eligible[0]['weight'])

    def testMultipleEntries(self):
        numDonors = 5
        donors = []
        prize = randgen.generate_prize(self.rand, event=self.event)
        prize.save()
        for i in range(0, numDonors):
            donor = randgen.generate_donor(self.rand)
            donor.save()
            entry = models.DonorPrizeEntry(donor=donor, prize=prize)
            entry.save()
            donors.append(donor.pk)
        eligible = prize.eligible_donors()
        self.assertEqual(numDonors, len(eligible))
        for donorId in [x['donor'] for x in eligible]:
            self.assertTrue(donorId in donors)


class TestPrizeMultiWin(TransactionTestCase):
    def setUp(self):
        self.eventStart = parse_date('2012-01-01 01:00:00Z')
        self.rand = random.Random()
        self.event = randgen.build_random_event(self.rand, start_time=self.eventStart)
        self.event.save()

    def testWinMultiPrize(self):
        donor = randgen.generate_donor(self.rand)
        donor.save()
        prize = randgen.generate_prize(self.rand)
        prize.event = self.event
        prize.maxwinners = 3
        prize.maxmultiwin = 3
        prize.save()
        models.DonorPrizeEntry.objects.create(donor=donor, prize=prize)
        result, msg = prizeutil.draw_prize(prize)
        self.assertTrue(result, msg)
        prizeWinner = models.PrizeWinner.objects.get(winner=donor, prize=prize)
        self.assertEqual(1, prizeWinner.pendingcount)
        result, msg = prizeutil.draw_prize(prize)
        self.assertTrue(result, msg)
        prizeWinner = models.PrizeWinner.objects.get(winner=donor, prize=prize)
        self.assertEqual(2, prizeWinner.pendingcount)
        result, msg = prizeutil.draw_prize(prize)
        self.assertTrue(result, msg)
        prizeWinner = models.PrizeWinner.objects.get(winner=donor, prize=prize)
        self.assertEqual(3, prizeWinner.pendingcount)
        result, msg = prizeutil.draw_prize(prize)
        self.assertFalse(result, msg)

    def testWinMultiPrizeWithAccept(self):
        donor = randgen.generate_donor(self.rand)
        donor.save()
        prize = randgen.generate_prize(self.rand)
        prize.event = self.event
        prize.maxwinners = 3
        prize.maxmultiwin = 3
        prize.save()
        models.DonorPrizeEntry.objects.create(donor=donor, prize=prize)
        prizeWinner = models.PrizeWinner.objects.create(
            winner=donor, prize=prize, pendingcount=1, acceptcount=1
        )
        result, msg = prizeutil.draw_prize(prize)
        self.assertTrue(result)
        prizeWinner = models.PrizeWinner.objects.get(winner=donor, prize=prize)
        self.assertEqual(2, prizeWinner.pendingcount)
        result, msg = prizeutil.draw_prize(prize)
        self.assertFalse(result)

    def testWinMultiPrizeWithDeny(self):
        donor = randgen.generate_donor(self.rand)
        donor.save()
        prize = randgen.generate_prize(self.rand)
        prize.event = self.event
        prize.maxwinners = 3
        prize.maxmultiwin = 3
        prize.save()
        models.DonorPrizeEntry.objects.create(donor=donor, prize=prize)
        prizeWinner = models.PrizeWinner.objects.create(
            winner=donor, prize=prize, pendingcount=1, declinecount=1
        )
        result, msg = prizeutil.draw_prize(prize)
        self.assertTrue(result)
        prizeWinner = models.PrizeWinner.objects.get(winner=donor, prize=prize)
        self.assertEqual(2, prizeWinner.pendingcount)
        result, msg = prizeutil.draw_prize(prize)
        self.assertFalse(result)

    def testWinMultiPrizeLowerThanMaxWin(self):
        donor = randgen.generate_donor(self.rand)
        donor.save()
        prize = randgen.generate_prize(self.rand)
        prize.event = self.event
        prize.maxwinners = 3
        prize.maxmultiwin = 2
        prize.save()
        models.DonorPrizeEntry.objects.create(donor=donor, prize=prize)
        prizeWinner = models.PrizeWinner.objects.create(
            winner=donor, prize=prize, pendingcount=1, declinecount=1
        )
        result, msg = prizeutil.draw_prize(prize)
        self.assertFalse(result)
        donor2 = randgen.generate_donor(self.rand)
        donor2.save()
        models.DonorPrizeEntry.objects.create(donor=donor2, prize=prize)
        result, msg = prizeutil.draw_prize(prize)
        self.assertTrue(result)
        prizeWinner = models.PrizeWinner.objects.get(winner=donor2, prize=prize)
        self.assertEqual(1, prizeWinner.pendingcount)
        result, msg = prizeutil.draw_prize(prize)
        self.assertTrue(result)
        result, msg = prizeutil.draw_prize(prize)
        self.assertFalse(result)


class TestPersistentPrizeWinners(TransactionTestCase):
    def setUp(self):
        self.rand = random.Random(None)
        self.event = randgen.generate_event(self.rand)
        self.event.save()

    # checks that a prize with a single eligible winner keeps track of a
    # declined prize, and disallows that person from being drawn again

    def test_decline_prize_single(self):
        amount = Decimal('50.0')
        targetPrize = randgen.generate_prize(
            self.rand,
            event=self.event,
            sum_donations=False,
            random_draw=False,
            min_amount=amount,
            max_amount=amount,
            maxwinners=1,
        )
        targetPrize.save()
        self.assertEqual(0, len(targetPrize.eligible_donors()))
        donorA = randgen.generate_donor(self.rand)
        donorA.save()
        donorB = randgen.generate_donor(self.rand)
        donorB.save()
        donationA = randgen.generate_donation(
            self.rand,
            donor=donorA,
            min_amount=amount,
            max_amount=amount,
            event=self.event,
        )
        donationA.save()
        self.assertEqual(1, len(targetPrize.eligible_donors()))
        self.assertEqual(donorA.id, targetPrize.eligible_donors()[0]['donor'])
        prizeutil.draw_prize(targetPrize)
        self.assertEqual(donorA, targetPrize.get_winner())
        self.assertEqual(0, len(targetPrize.eligible_donors()))
        donationB = randgen.generate_donation(
            self.rand,
            donor=donorB,
            min_amount=amount,
            max_amount=amount,
            event=self.event,
        )
        donationB.save()
        self.assertEqual(1, len(targetPrize.eligible_donors()))
        self.assertEqual(donorB.id, targetPrize.eligible_donors()[0]['donor'])
        prizeWinnerEntry = targetPrize.prizewinner_set.filter(winner=donorA)[0]
        prizeWinnerEntry.pendingcount = 0
        prizeWinnerEntry.declinecount = 1
        prizeWinnerEntry.save()
        self.assertEqual(1, len(targetPrize.eligible_donors()))
        self.assertEqual(donorB.id, targetPrize.eligible_donors()[0]['donor'])
        prizeutil.draw_prize(targetPrize)
        self.assertEqual(donorB, targetPrize.get_winner())
        self.assertEqual(1, targetPrize.current_win_count())
        self.assertEqual(0, len(targetPrize.eligible_donors()))

    def test_cannot_exceed_max_winners(self):
        targetPrize = randgen.generate_prize(self.rand, event=self.event)
        targetPrize.maxwinners = 2
        targetPrize.save()
        numDonors = 4
        donors = []
        for i in range(0, numDonors):
            donor = randgen.generate_donor(self.rand)
            donor.save()
            donors.append(donor)
        pw0 = models.PrizeWinner(winner=donors[0], prize=targetPrize)
        pw0.clean()
        pw0.save()
        pw1 = models.PrizeWinner(winner=donors[1], prize=targetPrize)
        pw1.clean()
        pw1.save()
        with self.assertRaises(ValidationError):
            pw2 = models.PrizeWinner(winner=donors[2], prize=targetPrize)
            pw2.clean()
        pw0.pendingcount = 0
        pw0.declinecount = 1
        pw0.save()
        pw2.clean()
        pw2.save()


class TestPrizeCountryFilter(TransactionTestCase):
    fixtures = ['countries']

    def setUp(self):
        self.rand = random.Random(None)
        self.event = randgen.build_random_event(self.rand)
        self.event.save()

    def testCountryFilterEvent(self):
        countries = list(models.Country.objects.all()[0:4])
        self.event.allowed_prize_countries.add(countries[0])
        self.event.allowed_prize_countries.add(countries[1])
        self.event.save()
        prize = models.Prize.objects.create(event=self.event)
        donors = []
        for country in countries:
            donor = randgen.generate_donor(self.rand)
            donor.addresscountry = country
            donor.save()
            donors.append(donor)
            randgen.generate_donation(
                self.rand,
                event=self.event,
                donor=donor,
                min_amount=Decimal(prize.minimumbid),
            ).save()

        self.assertTrue(prize.is_donor_allowed_to_receive(donors[0]))
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[1]))
        self.assertFalse(prize.is_donor_allowed_to_receive(donors[2]))
        self.assertFalse(prize.is_donor_allowed_to_receive(donors[3]))
        eligible = prize.eligible_donors()
        self.assertEqual(2, len(eligible))
        # Test a different country set
        self.event.allowed_prize_countries.add(countries[3])
        self.event.save()
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[0]))
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[1]))
        self.assertFalse(prize.is_donor_allowed_to_receive(donors[2]))
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[3]))
        eligible = prize.eligible_donors()
        self.assertEqual(3, len(eligible))
        # Test a blank country set
        self.event.allowed_prize_countries.clear()
        self.event.save()
        for donor in donors:
            self.assertTrue(prize.is_donor_allowed_to_receive(donor))
        eligible = prize.eligible_donors()
        self.assertEqual(4, len(eligible))

    def testCountryFilterPrize(self):
        # TODO: fix this so either there's less boilerplate, or the boilerplate is shared
        countries = list(models.Country.objects.all()[0:4])
        prize = models.Prize.objects.create(event=self.event)
        for country in countries[0:3]:
            self.event.allowed_prize_countries.add(country)
        self.event.save()
        prize.allowed_prize_countries.add(countries[0])
        prize.allowed_prize_countries.add(countries[1])
        prize.save()
        donors = []
        for country in countries:
            donor = randgen.generate_donor(self.rand)
            donor.addresscountry = country
            donor.save()
            donors.append(donor)
            randgen.generate_donation(
                self.rand,
                event=self.event,
                donor=donor,
                min_amount=Decimal(prize.minimumbid),
            ).save()
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[0]))
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[1]))
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[2]))
        self.assertFalse(prize.is_donor_allowed_to_receive(donors[3]))
        # by default don't use the prize filter
        eligible = prize.eligible_donors()
        self.assertEqual(3, len(eligible))

        prize.custom_country_filter = True
        prize.save()
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[0]))
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[1]))
        self.assertFalse(prize.is_donor_allowed_to_receive(donors[2]))
        self.assertFalse(prize.is_donor_allowed_to_receive(donors[3]))
        eligible = prize.eligible_donors()
        self.assertEqual(2, len(eligible))
        # Test a different country set
        prize.allowed_prize_countries.add(countries[3])
        prize.save()
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[0]))
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[1]))
        self.assertFalse(prize.is_donor_allowed_to_receive(donors[2]))
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[3]))
        eligible = prize.eligible_donors()
        self.assertEqual(3, len(eligible))
        # Test a blank country set
        prize.allowed_prize_countries.clear()
        prize.save()
        for donor in donors:
            self.assertTrue(prize.is_donor_allowed_to_receive(donor))
        eligible = prize.eligible_donors()
        self.assertEqual(4, len(eligible))

    def testCountryRegionBlacklistFilterEvent(self):
        # Somewhat ethnocentric testing
        country = models.Country.objects.all()[0]
        prize = models.Prize.objects.create(event=self.event)
        donors = []
        allowedState = 'StateOne'
        disallowedState = 'StateTwo'
        for state in [allowedState, disallowedState]:
            donor = randgen.generate_donor(self.rand)
            donor.addresscountry = country
            donor.addressstate = state
            donor.save()
            donors.append(donor)
            randgen.generate_donation(
                self.rand,
                event=self.event,
                donor=donor,
                min_amount=Decimal(prize.minimumbid),
            ).save()

        for donor in donors:
            self.assertTrue(prize.is_donor_allowed_to_receive(donor))
        eligible = prize.eligible_donors()
        self.assertEqual(2, len(eligible))
        # Test a different country set
        countryRegion = models.CountryRegion.objects.create(
            country=country, name=disallowedState
        )
        self.event.disallowed_prize_regions.add(countryRegion)
        self.event.save()
        self.assertTrue(prize.is_donor_allowed_to_receive(donors[0]))
        self.assertFalse(prize.is_donor_allowed_to_receive(donors[1]))
        eligible = prize.eligible_donors()
        self.assertEqual(1, len(eligible))

    def testCountryRegionBlacklistFilterPrize(self):
        # Somewhat ethnocentric testing
        country = models.Country.objects.all()[0]
        prize = models.Prize.objects.create(event=self.event)
        donors = []
        allowedState = 'StateOne'
        disallowedState = 'StateTwo'
        for state in [allowedState, disallowedState]:
            donor = randgen.generate_donor(self.rand)
            donor.addresscountry = country
            donor.addressstate = state
            donor.save()
            donors.append(donor)
            randgen.generate_donation(
                self.rand,
                event=self.event,
                donor=donor,
                min_amount=Decimal(prize.minimumbid),
            ).save()

        eligible = prize.eligible_donors()
        self.assertEqual(2, len(eligible))
        # Test a different country set
        countryRegion = models.CountryRegion.objects.create(
            country=country, name=disallowedState
        )
        prize.disallowed_prize_regions.add(countryRegion)
        prize.custom_country_filter = True
        prize.save()
        eligible = prize.eligible_donors()
        self.assertEqual(1, len(eligible))


class TestPrizeDrawAcceptOffset(TransactionTestCase):
    def setUp(self):
        self.rand = random.Random(None)
        self.event = randgen.generate_event(self.rand)
        self.event.save()

    def test_accept_deadline_offset(self):
        # 10 days in the future
        self.event.prize_accept_deadline_delta = 10
        # TODO: it should not take this much set-up to draw a single donor to a single prize
        amount = Decimal('50.0')
        targetPrize = randgen.generate_prize(
            self.rand,
            event=self.event,
            sum_donations=False,
            random_draw=False,
            min_amount=amount,
            max_amount=amount,
            maxwinners=1,
        )
        targetPrize.save()
        winner = randgen.generate_donor(self.rand)
        winner.save()
        winningDonation = randgen.generate_donation(
            self.rand,
            donor=winner,
            min_amount=amount,
            max_amount=amount,
            event=self.event,
        )
        winningDonation.save()
        self.assertEqual(1, len(targetPrize.eligible_donors()))
        self.assertEqual(winner.id, targetPrize.eligible_donors()[0]['donor'])
        self.assertEqual(0, len(prizeutil.get_past_due_prize_winners(self.event)))
        currentDate = datetime.date.today()
        result, status = prizeutil.draw_prize(targetPrize)
        prizeWin = models.PrizeWinner.objects.get(prize=targetPrize)
        self.assertEqual(
            prizeWin.accept_deadline_date(),
            currentDate
            + datetime.timedelta(days=self.event.prize_accept_deadline_delta),
        )

        prizeWin.acceptdeadline = datetime.datetime.utcnow().replace(
            tzinfo=pytz.utc
        ) - datetime.timedelta(days=2)
        prizeWin.save()
        self.assertEqual(0, len(targetPrize.eligible_donors()))
        pastDue = prizeutil.get_past_due_prize_winners(self.event)
        self.assertEqual(1, len(prizeutil.get_past_due_prize_winners(self.event)))
        self.assertEqual(prizeWin, pastDue[0])


class TestBackfillPrevNextMigrations(MigrationsTestCase):
    migrate_from = '0001_squashed_0020_add_runner_pronouns_and_platform'
    migrate_to = '0003_populate_prev_next_run'

    def setUpBeforeMigration(self, apps):
        Prize = apps.get_model('tracker', 'Prize')
        Event = apps.get_model('tracker', 'Event')
        SpeedRun = apps.get_model('tracker', 'SpeedRun')
        self.rand = random.Random(None)
        self.event = Event.objects.create(
            short='test', name='Test Event', datetime=today_noon, targetamount=100
        )
        self.run1 = SpeedRun.objects.create(
            event=self.event, name='Test Run 1', order=1, run_time='0:05:00'
        )
        self.run2 = SpeedRun.objects.create(
            event=self.event, name='Test Run 2', order=2, run_time='0:05:00'
        )
        self.run3 = SpeedRun.objects.create(
            event=self.event, name='Test Run 3', order=3, run_time='0:05:00'
        )
        self.prize1 = Prize.objects.create(
            event=self.event, name='Test Prize 1', startrun=self.run1, endrun=self.run1
        )
        self.prize2 = Prize.objects.create(
            event=self.event, name='Test Prize 2', startrun=self.run2, endrun=self.run2
        )
        self.prize3 = Prize.objects.create(
            event=self.event, name='Test Prize 3', startrun=self.run3, endrun=self.run3
        )

    def test_prev_next_backfilled(self):
        Prize = self.apps.get_model('tracker', 'Prize')
        prize1 = Prize.objects.get(pk=self.prize1.id)
        prize2 = Prize.objects.get(pk=self.prize2.id)
        prize3 = Prize.objects.get(pk=self.prize3.id)
        self.assertEqual(prize1.prev_run_id, None, 'prize 1 prev run incorrect')
        self.assertEqual(prize1.next_run_id, self.run2.id, 'prize 1 next run incorrect')
        self.assertEqual(prize2.prev_run_id, self.run1.id, 'prize 2 prev run incorrect')
        self.assertEqual(prize2.next_run_id, self.run3.id, 'prize 2 next run incorrect')
        self.assertEqual(prize3.prev_run_id, self.run2.id, 'prize 3 prev run incorrect')
        self.assertEqual(prize3.next_run_id, None, 'prize 3 next run incorrect')


class TestPrizeSignals(TestCase):
    def setUp(self):
        self.rand = random.Random(None)
        self.event = randgen.generate_event(self.rand)
        self.event.save()
        self.runs = randgen.generate_runs(self.rand, self.event, 4, scheduled=True)
        self.event_prize = models.Prize.objects.create(
            name='Event Wide Prize', startrun=self.runs[0], endrun=self.runs[3]
        )
        self.start_prize = models.Prize.objects.create(
            name='Start Prize', startrun=self.runs[0], endrun=self.runs[0]
        )
        self.middle_prize = models.Prize.objects.create(
            name='Middle Prize', startrun=self.runs[1], endrun=self.runs[1]
        )
        self.end_prize = models.Prize.objects.create(
            name='End Prize', startrun=self.runs[3], endrun=self.runs[3]
        )
        self.start_span_prize = models.Prize.objects.create(
            name='Start Span Prize', startrun=self.runs[0], endrun=self.runs[1]
        )
        self.middle_span_prize = models.Prize.objects.create(
            name='Middle Span Prize', startrun=self.runs[1], endrun=self.runs[2]
        )
        self.end_span_prize = models.Prize.objects.create(
            name='End Span Prize', startrun=self.runs[2], endrun=self.runs[3]
        )

    def refresh_all(self):
        for model in [
            self.event,
            self.event_prize,
            self.start_prize,
            self.middle_prize,
            self.end_prize,
            self.start_span_prize,
            self.middle_span_prize,
            self.end_span_prize,
        ] + self.runs:
            try:
                model.refresh_from_db()
            except ObjectDoesNotExist:
                pass  # deleted as part of test

    def test_initial_state(self):
        self.assertEqual(self.event_prize.prev_run, None)
        self.assertEqual(self.event_prize.next_run, None)
        self.assertEqual(self.start_prize.prev_run, None)
        self.assertEqual(self.start_prize.next_run, self.runs[1])
        self.assertEqual(self.middle_prize.prev_run, self.runs[0])
        self.assertEqual(self.middle_prize.next_run, self.runs[2])
        self.assertEqual(self.end_prize.prev_run, self.runs[2])
        self.assertEqual(self.end_prize.next_run, None)
        self.assertEqual(self.start_span_prize.prev_run, None)
        self.assertEqual(self.start_span_prize.next_run, self.runs[2])
        self.assertEqual(self.middle_span_prize.prev_run, self.runs[0])
        self.assertEqual(self.middle_span_prize.next_run, self.runs[3])
        self.assertEqual(self.end_span_prize.prev_run, self.runs[1])
        self.assertEqual(self.end_span_prize.next_run, None)

    def test_run_inserted(self):
        self.runs[3].order = 5
        self.runs[3].save()
        self.runs[2].order = 4
        self.runs[2].save()
        self.new_run = models.SpeedRun(
            event=self.event, name='New Run', run_time='0:05:00', order=3
        )
        self.new_run.save()
        self.refresh_all()
        self.assertEqual(self.event_prize.prev_run, None)
        self.assertEqual(self.event_prize.next_run, None)
        self.assertEqual(self.start_prize.prev_run, None)
        self.assertEqual(self.start_prize.next_run, self.runs[1])
        self.assertEqual(self.middle_prize.prev_run, self.runs[0])
        self.assertEqual(self.middle_prize.next_run, self.new_run)
        self.assertEqual(self.end_prize.prev_run, self.runs[2])
        self.assertEqual(self.end_prize.next_run, None)
        self.assertEqual(self.start_span_prize.prev_run, None)
        self.assertEqual(self.start_span_prize.next_run, self.new_run)
        self.assertEqual(self.middle_span_prize.prev_run, self.runs[0])
        self.assertEqual(self.middle_span_prize.next_run, self.runs[3])
        self.assertEqual(self.end_span_prize.prev_run, self.new_run)
        self.assertEqual(self.end_span_prize.next_run, None)

    def test_first_run_removed_from_order(self):
        self.runs[0].order = None
        self.runs[0].save()
        self.refresh_all()
        self.assertEqual(self.event_prize.prev_run, None)
        self.assertEqual(self.event_prize.next_run, None)
        self.assertEqual(self.start_prize.prev_run, None)
        self.assertEqual(self.start_prize.next_run, None)
        self.assertEqual(self.middle_prize.prev_run, None)
        self.assertEqual(self.middle_prize.next_run, self.runs[2])
        self.assertEqual(self.end_prize.prev_run, self.runs[2])
        self.assertEqual(self.end_prize.next_run, None)
        self.assertEqual(self.start_span_prize.prev_run, None)
        self.assertEqual(self.start_span_prize.next_run, None)
        self.assertEqual(self.middle_span_prize.prev_run, None)
        self.assertEqual(self.middle_span_prize.next_run, self.runs[3])
        self.assertEqual(self.end_span_prize.prev_run, self.runs[1])
        self.assertEqual(self.end_span_prize.next_run, None)

    def test_first_run_deleted(self):
        self.event_prize.startrun = self.runs[1]
        self.event_prize.save()
        self.start_prize.delete()
        self.start_span_prize.delete()
        self.runs[0].delete()
        self.refresh_all()
        self.assertEqual(self.event_prize.prev_run, None)
        self.assertEqual(self.event_prize.next_run, None)
        self.assertEqual(self.middle_prize.prev_run, None)
        self.assertEqual(self.middle_prize.next_run, self.runs[2])
        self.assertEqual(self.end_prize.prev_run, self.runs[2])
        self.assertEqual(self.end_prize.next_run, None)
        self.assertEqual(self.middle_span_prize.prev_run, None)
        self.assertEqual(self.middle_span_prize.next_run, self.runs[3])
        self.assertEqual(self.end_span_prize.prev_run, self.runs[1])
        self.assertEqual(self.end_span_prize.next_run, None)

    def test_second_run_removed_from_order(self):
        self.runs[1].order = None
        self.runs[1].save()
        self.refresh_all()
        self.assertEqual(self.event_prize.prev_run, None)
        self.assertEqual(self.event_prize.next_run, None)
        self.assertEqual(self.start_prize.prev_run, None)
        self.assertEqual(self.start_prize.next_run, self.runs[2])
        self.assertEqual(self.middle_prize.prev_run, None)
        self.assertEqual(self.middle_prize.next_run, None)
        self.assertEqual(self.end_prize.prev_run, self.runs[2])
        self.assertEqual(self.end_prize.next_run, None)
        self.assertEqual(self.start_span_prize.prev_run, None)
        self.assertEqual(self.start_span_prize.next_run, None)
        self.assertEqual(self.middle_span_prize.prev_run, None)
        self.assertEqual(self.middle_span_prize.next_run, None)
        self.assertEqual(self.end_span_prize.prev_run, self.runs[0])
        self.assertEqual(self.end_span_prize.next_run, None)

    def test_second_run_deleted(self):
        self.start_span_prize.delete()
        self.middle_prize.delete()
        self.middle_span_prize.delete()
        self.runs[1].delete()
        self.refresh_all()
        self.assertEqual(self.event_prize.prev_run, None)
        self.assertEqual(self.event_prize.next_run, None)
        self.assertEqual(self.start_prize.prev_run, None)
        self.assertEqual(self.start_prize.next_run, self.runs[2])
        self.assertEqual(self.end_prize.prev_run, self.runs[2])
        self.assertEqual(self.end_prize.next_run, None)
        self.assertEqual(self.end_span_prize.prev_run, self.runs[0])
        self.assertEqual(self.end_span_prize.next_run, None)

    def test_third_run_removed_from_order(self):
        self.runs[2].order = None
        self.runs[2].save()
        self.refresh_all()
        self.assertEqual(self.event_prize.prev_run, None)
        self.assertEqual(self.event_prize.next_run, None)
        self.assertEqual(self.start_prize.prev_run, None)
        self.assertEqual(self.start_prize.next_run, self.runs[1])
        self.assertEqual(self.middle_prize.prev_run, self.runs[0])
        self.assertEqual(self.middle_prize.next_run, self.runs[3])
        self.assertEqual(self.end_prize.prev_run, self.runs[1])
        self.assertEqual(self.end_prize.next_run, None)
        self.assertEqual(self.start_span_prize.prev_run, None)
        self.assertEqual(self.start_span_prize.next_run, self.runs[3])
        self.assertEqual(self.middle_span_prize.prev_run, None)
        self.assertEqual(self.middle_span_prize.next_run, None)
        self.assertEqual(self.end_span_prize.prev_run, None)
        self.assertEqual(self.end_span_prize.next_run, None)

    def test_third_run_deleted(self):
        self.middle_span_prize.delete()
        self.end_span_prize.delete()
        self.runs[2].delete()
        self.refresh_all()
        self.assertEqual(self.event_prize.prev_run, None)
        self.assertEqual(self.event_prize.next_run, None)
        self.assertEqual(self.start_prize.prev_run, None)
        self.assertEqual(self.start_prize.next_run, self.runs[1])
        self.assertEqual(self.middle_prize.prev_run, self.runs[0])
        self.assertEqual(self.middle_prize.next_run, self.runs[3])
        self.assertEqual(self.end_prize.prev_run, self.runs[1])
        self.assertEqual(self.end_prize.next_run, None)
        self.assertEqual(self.start_span_prize.prev_run, None)
        self.assertEqual(self.start_span_prize.next_run, self.runs[3])

    def test_fourth_run_removed_from_order(self):
        self.runs[3].order = None
        self.runs[3].save()
        self.refresh_all()
        self.assertEqual(self.event_prize.prev_run, None)
        self.assertEqual(self.event_prize.next_run, None)
        self.assertEqual(self.start_prize.prev_run, None)
        self.assertEqual(self.start_prize.next_run, self.runs[1])
        self.assertEqual(self.middle_prize.prev_run, self.runs[0])
        self.assertEqual(self.middle_prize.next_run, self.runs[2])
        self.assertEqual(self.end_prize.prev_run, None)
        self.assertEqual(self.end_prize.next_run, None)
        self.assertEqual(self.start_span_prize.prev_run, None)
        self.assertEqual(self.start_span_prize.next_run, self.runs[2])
        self.assertEqual(self.middle_span_prize.prev_run, self.runs[0])
        self.assertEqual(self.middle_span_prize.next_run, None)
        self.assertEqual(self.end_span_prize.prev_run, None)
        self.assertEqual(self.end_span_prize.next_run, None)

    def test_fourth_run_deleted(self):
        self.end_prize.delete()
        self.end_span_prize.delete()
        self.event_prize.endrun = self.runs[2]
        self.event_prize.save()
        self.runs[3].delete()
        self.refresh_all()
        self.assertEqual(self.event_prize.prev_run, None)
        self.assertEqual(self.event_prize.next_run, None)
        self.assertEqual(self.start_prize.prev_run, None)
        self.assertEqual(self.start_prize.next_run, self.runs[1])
        self.assertEqual(self.middle_prize.prev_run, self.runs[0])
        self.assertEqual(self.middle_prize.next_run, self.runs[2])
        self.assertEqual(self.start_span_prize.prev_run, None)
        self.assertEqual(self.start_span_prize.next_run, self.runs[2])
        self.assertEqual(self.middle_span_prize.prev_run, self.runs[0])
        self.assertEqual(self.middle_span_prize.next_run, None)


class TestPrizeTimeRange(TestCase):
    def setUp(self):
        self.rand = random.Random(None)
        self.event = randgen.generate_event(self.rand)
        self.event.save()
        self.runs = randgen.generate_runs(self.rand, self.event, 4, scheduled=True)


class TestPrizeKey(TestCase):
    def setUp(self):
        self.rand = random.Random(None)
        self.event = randgen.generate_event(self.rand)
        self.event.save()
        self.run = randgen.generate_run(self.rand, event=self.event)
        self.run.order = 1
        self.run.save()
        self.prize = randgen.generate_prize(
            self.rand,
            event=self.event,
            start_run=self.run,
            end_run=self.run,
            random_draw=True,
        )
        self.prize.key_code = True
        self.prize.save()
        models.PrizeKey.objects.bulk_create(
            randgen.generate_prize_key(self.rand, prize=self.prize) for _ in range(100)
        )
        self.prize_keys = self.prize.prizekey_set.all()

    def test_leave_winners_alone_for_non_key_code(self):
        self.prize.key_code = False
        self.prize.maxwinners = 10
        self.prize.save()
        self.assertEqual(self.prize.maxwinners, 10)

    def test_set_winners_to_key_number_on_prize_save(self):
        self.assertEqual(self.prize.maxwinners, 0)
        self.prize.maxmultiwin = 5
        self.prize.save()
        self.assertEqual(self.prize.maxwinners, self.prize_keys.count())
        self.assertEqual(self.prize.maxmultiwin, 1)

    def test_set_winners_to_key_number_on_prize_key_create(self):
        self.assertEqual(self.prize.maxwinners, 0)
        self.prize_keys[0].save()  # only on create
        self.prize.refresh_from_db()
        self.assertEqual(self.prize.maxwinners, 0)
        randgen.generate_prize_key(self.rand, prize=self.prize).save()
        self.prize.refresh_from_db()
        self.assertEqual(self.prize.maxwinners, self.prize_keys.count())
        self.assertEqual(self.prize.maxmultiwin, 1)

    def test_fewer_donors_than_keys(self):
        self.prize.save()
        donor_count = self.prize_keys.count() // 2
        models.Donor.objects.bulk_create(
            [randgen.generate_donor(self.rand) for _ in range(donor_count)]
        )
        # only Postgres returns the objects with pks, so refetch
        donors = list(models.Donor.objects.order_by('-id')[:donor_count])
        models.Donation.objects.bulk_create(
            [
                randgen.generate_donation_for_prize(
                    self.rand, donor=d, prize=self.prize
                )
                for d in donors
            ]
        )
        self.assertSetEqual(
            {d['donor'] for d in self.prize.eligible_donors()}, {d.id for d in donors}
        )
        success, result = prizeutil.draw_keys(self.prize, rand=self.rand)
        self.assertTrue(success, result)
        self.assertSetEqual(set(result['winners']), {d.id for d in donors})
        self.assertSetEqual(
            {k.winner.id for k in self.prize_keys if k.winner}, {d.id for d in donors}
        )
        for key in self.prize_keys:
            if not key.winner:
                continue
            self.assertIn(key.winner, donors, '%s was not in donors.' % key.winner)
            self.assertEqual(key.prize_winner.pendingcount, 0)
            self.assertEqual(key.prize_winner.acceptcount, 1)
            self.assertEqual(key.prize_winner.declinecount, 0)
            self.assertTrue(key.prize_winner.emailsent)
            self.assertEqual(key.prize_winner.acceptemailsentcount, 1)
            self.assertEqual(key.prize_winner.shippingstate, 'SHIPPED')
            self.assertFalse(key.prize_winner.shippingemailsent)

    def test_draw_with_claimed_keys(self):
        self.prize.save()
        donor_count = self.prize_keys.count() // 2
        models.Donor.objects.bulk_create(
            [randgen.generate_donor(self.rand) for _ in range(donor_count)]
        )
        # only Postgres returns the objects with pks, so refetch
        old_donors = set(models.Donor.objects.order_by('-id')[:donor_count])
        old_ids = {d.id for d in old_donors}
        models.Donation.objects.bulk_create(
            [
                randgen.generate_donation_for_prize(
                    self.rand, donor=d, prize=self.prize
                )
                for d in old_donors
            ]
        )
        self.assertSetEqual(
            {d['donor'] for d in self.prize.eligible_donors()},
            {d.id for d in old_donors},
        )
        success, result = prizeutil.draw_keys(self.prize, rand=self.rand)
        self.assertTrue(success, result)
        models.Donor.objects.bulk_create(
            [randgen.generate_donor(self.rand) for _ in range(donor_count)]
        )
        new_donors = set(models.Donor.objects.order_by('-id')[:donor_count])
        models.Donation.objects.bulk_create(
            [
                randgen.generate_donation_for_prize(
                    self.rand, donor=d, prize=self.prize
                )
                for d in new_donors
            ]
        )
        self.assertSetEqual(
            {d['donor'] for d in self.prize.eligible_donors()},
            {d.id for d in new_donors},
        )
        success, result = prizeutil.draw_keys(self.prize, rand=self.rand)
        self.assertTrue(success, result)
        self.assertSetEqual(set(result['winners']), {d.id for d in new_donors})
        self.assertSetEqual(
            {k.winner.id for k in self.prize_keys if k.winner},
            old_ids | {d.id for d in new_donors},
        )
        all_donors = old_donors | new_donors
        for key in self.prize_keys:
            self.assertIn(key.winner, all_donors, '%s was not in donors.' % key.winner)
            self.assertEqual(key.prize_winner.pendingcount, 0)
            self.assertEqual(key.prize_winner.acceptcount, 1)
            self.assertEqual(key.prize_winner.declinecount, 0)
            self.assertTrue(key.prize_winner.emailsent)
            self.assertEqual(key.prize_winner.acceptemailsentcount, 1)
            self.assertEqual(key.prize_winner.shippingstate, 'SHIPPED')
            self.assertFalse(key.prize_winner.shippingemailsent)

    def test_more_donors_than_keys(self):
        self.prize.save()
        donor_count = self.prize_keys.count() * 2
        models.Donor.objects.bulk_create(
            [randgen.generate_donor(self.rand) for _ in range(donor_count)]
        )
        # only Postgres returns the objects with pks, so refetch
        donors = list(models.Donor.objects.order_by('id')[:donor_count])
        models.Donation.objects.bulk_create(
            [
                randgen.generate_donation_for_prize(
                    self.rand, donor=d, prize=self.prize
                )
                for d in donors
            ]
        )
        self.assertSetEqual(
            {d['donor'] for d in self.prize.eligible_donors()}, {d.id for d in donors}
        )
        success, result = prizeutil.draw_keys(self.prize, rand=self.rand)
        self.assertTrue(success, result)
        self.assertEqual(self.prize.prizewinner_set.count(), self.prize_keys.count())
        for key in self.prize_keys:
            self.assertIn(
                key.winner, donors, '%s was not in eligible donors.' % key.winner
            )
            self.assertIn(
                key.winner.id, result['winners'], '%s was not in winners.' % key.winner
            )
            self.assertEqual(key.prize_winner.pendingcount, 0)
            self.assertEqual(key.prize_winner.acceptcount, 1)
            self.assertEqual(key.prize_winner.declinecount, 0)
            self.assertTrue(key.prize_winner.emailsent)
            self.assertEqual(key.prize_winner.acceptemailsentcount, 1)
            self.assertEqual(key.prize_winner.shippingstate, 'SHIPPED')
            self.assertFalse(key.prize_winner.shippingemailsent)
        old_winners = sorted(result['winners'])
        old_donors = sorted(w.winner.id for w in self.prize.prizewinner_set.all())

        self.prize.prizekey_set.update(prize_winner=None)
        self.prize.prizewinner_set.all().delete()

        # assert actual randomness
        success, result = prizeutil.draw_keys(self.prize, rand=self.rand)
        self.assertTrue(success, result)
        self.assertNotEqual(sorted(result['winners']), old_winners)
        self.assertNotEqual(
            sorted(w.winner.id for w in self.prize.prizewinner_set.all()), old_donors
        )


class TestPrizeAdmin(TestCase):
    def assertMessages(self, response, messages):  # TODO: util?
        self.assertSetEqual(
            {str(m) for m in response.wsgi_request._messages}, set(messages)
        )

    def setUp(self):
        self.staff_user = User.objects.create_user(
            'staff', 'staff@example.com', 'staff'
        )
        self.super_user = User.objects.create_superuser(
            'admin', 'admin@example.com', 'password'
        )
        self.rand = random.Random(None)
        self.event = randgen.generate_event(self.rand)
        self.event.save()
        self.prize = randgen.generate_prize(self.rand, event=self.event)
        self.prize.maximumbid = self.prize.minimumbid + 5
        self.prize.save()
        # TODO: janky place to test this behavior, but it'll do for now
        self.assertEqual(self.prize.minimumbid, self.prize.maximumbid)
        self.prize_with_keys = randgen.generate_prize(self.rand, event=self.event)
        self.prize_with_keys.key_code = True
        self.prize_with_keys.save()
        self.donor = randgen.generate_donor(self.rand)
        self.donor.save()
        self.prize_winner = models.PrizeWinner.objects.create(
            winner=self.donor, prize=self.prize
        )
        self.donor_prize_entry = models.DonorPrizeEntry.objects.create(
            donor=self.donor, prize=self.prize
        )
        self.prize_key = models.PrizeKey.objects.create(
            prize=self.prize_with_keys, key='dead-beef-dead-beef'
        )

    def test_prize_admin(self):
        self.client.login(username='admin', password='password')
        response = self.client.get(reverse('admin:tracker_prize_changelist'))
        self.assertEqual(response.status_code, 200)
        response = self.client.get(reverse('admin:tracker_prize_add'))
        self.assertEqual(response.status_code, 200)
        response = self.client.get(
            reverse('admin:tracker_prize_change', args=(self.prize.id,))
        )
        self.assertEqual(response.status_code, 200)

    def test_prize_key_import_action(self):
        self.client.login(username='admin', password='password')

        response = self.client.post(
            reverse('admin:tracker_prize_changelist'),
            {
                'action': 'import_keys_action',
                ACTION_CHECKBOX_NAME: [self.prize.id, self.prize_with_keys.id],
            },
        )
        self.assertRedirects(response, reverse('admin:tracker_prize_changelist'))
        self.assertMessages(response, ['Select exactly one prize that uses keys.'])
        response = self.client.post(
            reverse('admin:tracker_prize_changelist'),
            {'action': 'import_keys_action', ACTION_CHECKBOX_NAME: [self.prize.id]},
        )
        self.assertRedirects(response, reverse('admin:tracker_prize_changelist'))
        self.assertMessages(response, ['Select exactly one prize that uses keys.'])
        response = self.client.post(
            reverse('admin:tracker_prize_changelist'),
            {
                'action': 'import_keys_action',
                ACTION_CHECKBOX_NAME: [self.prize_with_keys.id],
            },
        )
        self.assertRedirects(
            response,
            reverse('admin:tracker_prize_key_import', args=(self.prize_with_keys.id,)),
        )

    def test_prize_key_import_form(self):
        keys = ['dead-beef-dead-beef-123%d' % i for i in range(5)]
        response = self.client.get(
            reverse('admin:tracker_prize_key_import', args=(self.prize_with_keys.id,))
        )
        self.assertEqual(response.status_code, 302)

        self.client.login(username='staff', password='password')
        response = self.client.get(
            reverse('admin:tracker_prize_key_import', args=(self.prize_with_keys.id,))
        )
        self.assertEqual(response.status_code, 302)

        self.client.login(username='admin', password='password')
        response = self.client.get(
            reverse('admin:tracker_prize_key_import', args=(self.prize.id,))
        )
        self.assertRedirects(response, reverse('admin:tracker_prize_changelist'))
        self.assertMessages(response, ['Cannot import prize keys to non key prizes.'])

        response = self.client.get(
            reverse(
                'admin:tracker_prize_key_import', args=(self.prize_with_keys.id + 1,)
            )
        )
        self.assertEqual(response.status_code, 404)

        response = self.client.get(
            reverse('admin:tracker_prize_key_import', args=(self.prize_with_keys.id,))
        )
        self.assertEqual(response.status_code, 200)

        keys_input = (
            '\n' + ' \n '.join(keys) + '\n%s\n\n' % keys[0]
        )  # test whitespace stripping and deduping
        response = self.client.post(
            reverse('admin:tracker_prize_key_import', args=(self.prize_with_keys.id,)),
            {'keys': keys_input},
        )

        self.assertRedirects(response, reverse('admin:tracker_prize_changelist'))
        self.assertMessages(response, ['5 key(s) added to prize.'])
        self.prize_with_keys.refresh_from_db()
        self.assertEqual(self.prize_with_keys.maxwinners, 6)
        self.assertEqual(self.prize_with_keys.prizekey_set.count(), 6)
        self.assertSetEqual(
            set(keys), {key.key for key in self.prize_with_keys.prizekey_set.all()[1:]}
        )

        response = self.client.post(
            reverse('admin:tracker_prize_key_import', args=(self.prize_with_keys.id,)),
            {'keys': keys[0]},
        )
        self.assertFormError(
            response, 'form', 'keys', ['At least one key already exists.']
        )

    def test_prize_winner_admin(self):
        self.client.login(username='admin', password='password')
        response = self.client.get(reverse('admin:tracker_prizewinner_changelist'))
        self.assertEqual(response.status_code, 200)
        response = self.client.get(reverse('admin:tracker_prizewinner_add'))
        self.assertEqual(response.status_code, 200)
        response = self.client.get(
            reverse('admin:tracker_prizewinner_change', args=(self.prize_winner.id,))
        )
        self.assertEqual(response.status_code, 200)

    def test_donor_prize_entry_admin(self):
        self.client.login(username='admin', password='password')
        response = self.client.get(reverse('admin:tracker_donorprizeentry_changelist'))
        self.assertEqual(response.status_code, 200)
        response = self.client.get(reverse('admin:tracker_donorprizeentry_add'))
        self.assertEqual(response.status_code, 200)
        response = self.client.get(
            reverse(
                'admin:tracker_donorprizeentry_change',
                args=(self.donor_prize_entry.id,),
            )
        )
        self.assertEqual(response.status_code, 200)

    def test_prize_key_admin(self):
        self.client.login(username='admin', password='password')
        response = self.client.get(reverse('admin:tracker_prizekey_changelist'))
        self.assertEqual(response.status_code, 200)
        response = self.client.get(reverse('admin:tracker_prizekey_add'))
        self.assertEqual(response.status_code, 200)
        response = self.client.get(
            reverse('admin:tracker_prizekey_change', args=(self.prize_key.id,))
        )
        self.assertEqual(response.status_code, 200)

    def test_prize_mail_preview(self):
        self.client.login(username='admin', password='password')
        response = self.client.get(
            reverse('admin:preview_prize_winner_mail', args=(self.prize_winner.id,))
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(post_office.models.Email.objects.count(), 0)


class TestPrizeList(TestCase):
    def setUp(self):
        self.rand = random.Random(None)
        self.event = randgen.generate_event(self.rand, start_time=today_noon)
        self.event.save()

    def test_prize_list(self):
        regular_prize = randgen.generate_prize(
            self.rand, event=self.event, maxwinners=2
        )
        regular_prize.save()
        donors = randgen.generate_donors(self.rand, 2)
        for d in donors:
            models.PrizeWinner.objects.create(prize=regular_prize, winner=d)
        key_prize = randgen.generate_prize(self.rand, event=self.event)
        key_prize.key_code = True
        key_prize.save()
        key_winners = randgen.generate_donors(self.rand, 50)
        prize_keys = randgen.generate_prize_keys(self.rand, 50, prize=key_prize)
        for w, k in zip(key_winners, prize_keys):
            k.prize_winner = models.PrizeWinner.objects.create(
                prize=key_prize, winner=w
            )
            k.save()

        response = self.client.get(reverse('tracker:prizeindex', args=(self.event.id,)))
        self.assertContains(response, donors[0].visible_name())
        self.assertContains(response, donors[1].visible_name())
        self.assertContains(response, '50 winner(s)')
        self.assertNotContains(response, 'Invalid Variable')


class TestPrizeWinner(TestCase):
    def setUp(self):
        self.rand = random.Random(None)
        self.event = randgen.generate_event(self.rand, start_time=today_noon)
        self.event.save()
        randgen.generate_runs(self.rand, self.event, 1, scheduled=True)
        self.write_in_prize = randgen.generate_prizes(self.rand, self.event, 1)[0]
        self.write_in_donor = randgen.generate_donors(self.rand, 1)[0]
        models.PrizeWinner.objects.create(
            prize=self.write_in_prize, winner=self.write_in_donor, acceptcount=1
        )
        self.donation_prize = randgen.generate_prizes(self.rand, self.event, 1)[0]
        self.donation_donor = randgen.generate_donors(self.rand, 1)[0]
        models.Donation.objects.create(
            event=self.event,
            donor=self.donation_donor,
            transactionstate='COMPLETED',
            amount=5,
        )
        models.PrizeWinner.objects.create(
            prize=self.donation_prize, winner=self.donation_donor, acceptcount=1
        )

    def test_donor_cache(self):
        self.assertEqual(
            self.write_in_prize.get_prize_winner().donor_cache, self.write_in_donor
        )
        self.assertEqual(
            self.donation_prize.get_prize_winner().donor_cache,
            self.donation_donor.cache_for(self.event.id),
        )