# -*- coding: utf-8 -*-
# :Progetto:  SoL -- Basic tests on the DB modelization
# :Creato:    lun 22 set 2008 16:13:43 CEST
# :Autore:    Lele Gaifax <lele@metapensiero.it>
# :Licenza:   GNU General Public License version 3 or later
#

from decimal import Decimal
import logging
from operator import attrgetter

import transaction
from sqlalchemy.orm.exc import NoResultFound

from sol import models
from sol.models.errors import OperationAborted
from sol.tests import (
    ClubData,
    ONEDAY,
    PlayerData,
    RateData,
    RatingData,
    ChampionshipData,
    TODAY,
    TestCase,
    TourneyData,
    )


class TestModels(TestCase):
    def test_player(self):
        s = models.DBSession()
        juri = s.query(models.Player) \
               .filter_by(firstname=PlayerData.juri.firstname).one()
        self.assertEqual(juri.lastname, PlayerData.juri.lastname)
        self.assert_(juri.description)

        p = juri.partecipations()
        self.assert_(p)

        m = juri.matchesSummary()
        self.assertEqual(len(m), 3)

    def test_federated_player(self):
        s = models.DBSession()
        elisa = s.query(models.Player) \
               .filter_by(firstname=PlayerData.elisam.firstname).one()
        self.assertEqual(elisa.lastname, PlayerData.elisam.lastname)
        self.assertEqual(elisa.federation.description,
                         ClubData.fic.description)

    def test_competitor(self):
        s = models.DBSession()
        fcomp1 = s.query(models.Competitor).first()
        desc = fcomp1.description
        self.assertEqual(
            desc, "<b>Gaifas</b> Emanuele “Lele” and <b>Turchina</b> Fata")

    def test_match(self):
        s = models.DBSession()
        match = s.query(models.Match).first()
        desc = match.description
        self.assertIn('vs.', desc)

    def test_previous_championship(self):
        s = models.DBSession()
        championship = s.query(models.Championship) \
                 .filter_by(description=ChampionshipData.next.description).one()
        self.assertEqual(championship.previous.description, ChampionshipData.current.description)

    def test_merge(self):
        s = models.DBSession()
        juri = s.query(models.Player) \
                .filter_by(firstname=PlayerData.juri.firstname).one()
        self.assertIsNone(juri.sex)
        self.assertEqual(juri.merged, [])
        tobemerged = s.query(models.Player) \
                     .filter_by(firstname=PlayerData.merge1.firstname).all()
        tobemerged_guids = [p.guid for p in tobemerged]
        merge1_id, = [p.idplayer for p in tobemerged
                     if p.lastname == PlayerData.merge1.lastname]
        juri.mergePlayers([p.idplayer for p in tobemerged])
        s.flush()
        self.assertEqual(set(m.guid for m in juri.merged), set(tobemerged_guids))
        self.assertEqual(juri.sex, PlayerData.merge1.sex)
        self.assertEqual(juri.email, PlayerData.juri.email)
        tobemerged = s.query(models.Player) \
                     .filter_by(firstname=PlayerData.merge1.firstname).all()
        self.assertEqual(tobemerged, [])
        merge1_rates = s.query(models.Rate).filter_by(idplayer=merge1_id).all()
        self.assertEqual(merge1_rates, [])

        bob = s.query(models.Player) \
              .filter_by(firstname=PlayerData.bob.firstname).one()
        try:
            bob.mergePlayers([juri.idplayer])
        except OperationAborted:
            pass
        else:
            assert False, "Should have raised an error!"

    def test_merge_guid(self):
        s = models.DBSession()
        pk = s.query(models.Player) \
              .filter_by(lastname=PlayerData.pk.lastname).one()
        self.assertEqual(pk.merged, [])
        tobemerged = s.query(models.Player) \
                     .filter_by(firstname=PlayerData.merge3.firstname).all()
        tobemerged_guids = [p.guid for p in tobemerged]
        pk.mergePlayers(tobemerged_guids)
        s.flush()
        self.assertIn(tobemerged_guids[0], set(m.guid for m in pk.merged))
        tobemerged = s.query(models.Player) \
                     .filter_by(firstname=PlayerData.merge3.firstname).all()
        self.assertEqual(tobemerged, [])
        player, merged_into = models.Player.find(s,
                                                 PlayerData.merge3.lastname,
                                                 PlayerData.merge3.firstname)
        self.assertIs(player, pk)
        self.assertTrue(merged_into)

    def test_rating(self):
        s = models.DBSession()
        rating = s.query(models.Rating) \
                 .filter_by(description=RatingData.national.description).one()
        self.assertEqual(rating.description, RatingData.national.description)

    def test_guid(self):
        s = models.DBSession()
        juri = s.query(models.Player) \
               .filter_by(firstname=PlayerData.juri.firstname).one()
        self.assertIsNotNone(juri.modified)
        self.assertIsNotNone(juri.guid)

    def test_readonly_guid(self):
        s = models.DBSession()
        juri = s.query(models.Player) \
               .filter_by(firstname=PlayerData.juri.firstname).one()
        with self.assertRaises(ValueError):
            juri.guid = 'foo'


class TestRating(TestCase):
    def test_rates(self):
        s = models.DBSession()

        lele = s.query(models.Player) \
               .filter_by(nickname=PlayerData.lele.nickname).one()

        rating = s.query(models.Rating) \
                 .filter_by(description=RatingData.national.description).one()

        latest_rate = rating.getPlayerRating(lele)
        self.assertEqual(latest_rate.mu, RateData.lele_tomorrow.rate)

        previous_rate = rating.getPlayerRating(lele, RateData.lele_today.date)
        self.assertEqual(previous_rate.mu, RateData.lele_yesterday.rate)

    def test_ratings_level(self):
        s = models.DBSession()

        varechina = s.query(models.Player) \
                     .filter_by(lastname=PlayerData.varechina.lastname).one()

        rating = s.query(models.Rating) \
                 .filter_by(description=RatingData.national.description).one()

        varechina_rate = rating.getPlayerRating(varechina)
        self.assertEqual(varechina_rate.mu, RateData.varechina_european.rate)

    def test_standalone_ratings_level(self):
        s = models.DBSession()

        varechina = s.query(models.Player) \
                     .filter_by(lastname=PlayerData.varechina.lastname).one()

        rating = s.query(models.Rating) \
                 .filter_by(description=RatingData.standalone.description).one()

        varechina_rate = rating.getPlayerRating(varechina)
        self.assertEqual(varechina_rate.mu, RateData.varechina_standalone.rate)

    def test_competitor(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.rated.description).one()
        c = t.competitors[0]
        self.assertEqual(c.player1.nickname, PlayerData.lele.nickname)
        self.assertEqual(c.rate, RateData.lele_tomorrow.rate)

    def test_recompute(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.rated.description).one()
        t.assignPrizes()

        c = t.competitors[0]
        r = t.rating.getPlayerRating(c.player1)
        self.assertEqual(r.rate, 1492)
        self.assertEqual(r.deviation, 151)
        self.assertEqual(r.volatility, Decimal('0.05999'))

    def test_recompute_exponential(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.rated_exponential.description).one()
        ranking = t.ranking
        self.assertEqual(len(ranking), 2)
        t.assignPrizes()
        c = t.competitors[0]
        r = t.rating.getPlayerRating(c.player1)
        self.assertEqual(r.rate, 1595)

    def test_first_turn(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.rated_no_turns.description).one()
        self.assertEqual(t.matches, [])
        t.makeNextTurn()
        self.assertEqual(len(t.matches), (len(t.competitors) + 1) // 2)
        fm = t.matches[0]
        self.assertEqual(fm.turn, 1)
        self.assertEqual(fm.competitor1.player1.firstname,
                         PlayerData.fabiot.firstname)
        self.assertEqual(fm.competitor2.player1.firstname,
                         PlayerData.lucab.firstname)

    def test_first_turn_odd(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.rated_no_turns_odd.description).one()
        self.assertEqual(t.matches, [])
        byrevrate = list(sorted(t.competitors, key=attrgetter('rate'), reverse=True))
        self.assertFalse(any(c.rate==1500 for c in t.competitors))
        t.makeNextTurn()
        self.assertEqual(len(t.matches), (len(t.competitors) + 1) // 2)
        fm = t.matches[0]
        self.assertEqual(fm.turn, 1)
        self.assertIs(fm.competitor1.player1, byrevrate[0].player1)
        self.assertIs(fm.competitor2.player1, byrevrate[3].player1)
        fm = t.matches[1]
        self.assertIs(fm.competitor1.player1, byrevrate[1].player1)
        self.assertIs(fm.competitor2.player1, byrevrate[4].player1)
        fm = t.matches[2]
        self.assertIs(fm.competitor1.player1, byrevrate[2].player1)
        self.assertIs(fm.competitor2.player1, byrevrate[5].player1)
        fm = t.matches[3]
        self.assertIs(fm.competitor1.player1, byrevrate[6].player1)
        self.assertIs(fm.competitor2, None) # Phantom

    def test_ranking(self):
        s = models.DBSession()

        rating = s.query(models.Rating) \
                 .filter_by(description=RatingData.national.description).one()

        ranking = rating.ranking

        self.assertEqual(ranking[0][0].firstname, PlayerData.juri.firstname)
        self.assertEqual(ranking[0][1], 1700)
        self.assertEqual(ranking[-1][0].firstname, PlayerData.varechina.firstname)
        self.assertEqual(ranking[-1][1], 1200)

    def test_timespan(self):
        s = models.DBSession()

        rating = s.query(models.Rating) \
                 .filter_by(description=RatingData.european.description).one()

        self.assertEqual(rating.time_span, (TODAY-ONEDAY*2, TODAY))

    def test_outcomes(self):
        for oc in ('glicko', 'guido', 'expds'):
            r = models.Rating(outcomes=oc)
            compute_outcomes = getattr(r, "_compute%sOutcomes" % oc.capitalize())
            self.assertEqual(compute_outcomes(25, 0), (1, 0))
            self.assertEqual(compute_outcomes(0, 25), (0, 1))
            for s in range(26):
                self.assertEqual(compute_outcomes(s, s), (0.5, 0.5))


class TestTourney(TestCase):
    def test_tourney(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.first.description).one()
        self.assertEqual(t.description, TourneyData.first.description)
        self.assertEqual(t.championship.description, ChampionshipData.current.description)
        self.assertEqual(len(t.competitors), 6)

    def test_ranking(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.first.description).one()
        ranking = t.ranking
        self.assertEqual(len(ranking), 6)
        first = ranking[0].player1
        self.assertEqual(first.lastname, PlayerData.blond.lastname)

    def test_first_turn(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.second.description).one()
        self.assertEqual(t.matches, [])
        t.makeNextTurn()
        self.assertEqual(len(t.matches), (len(t.competitors) + 1) // 2)

    def test_next_turn(self):
        from random import randint
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.first.description).one()
        t.prized = False
        lastturn = t.currentturn
        t.makeNextTurn()
        self.assertEqual(t.currentturn, lastturn+1)
        self.assertEqual(len(t.matches), 12)
        lastturn = t.currentturn
        for m in t.matches:
            if m.turn == lastturn:
                m.score1 = randint(1, 25)
                m.score2 = randint(1, 25)
        t.makeNextTurn()
        self.assertEqual(t.currentturn, lastturn+1)
        self.assertEqual(t.rankedturn, lastturn)
        self.assertEqual(len(t.matches), 15)
        # Here we cannot generate the next turn, because have
        # non-scored matches
        try:
            t.makeNextTurn()
        except OperationAborted:
            # The ranking should not fail, just ignore the
            # not yet scored turn
            t.ranking
            self.assertEqual(t.currentturn, t.rankedturn+1)
        else:
            assert False, "Should have raised an error!"

    def test_next_turn_few_players(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.apr24.description).one()
        best = t.ranking[0]
        s.flush()
        self.assertEqual(best.player1.firstname, 'Emanuele')
        self.assertEqual(len(t.matches), 20)

        lastturn = t.currentturn
        t.makeNextTurn()
        self.assertEqual(t.currentturn, lastturn+1)
        self.assertEqual(len(t.matches), 24)

    def test_odd(self):
        from random import randint

        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.odd.description).one()
        self.assertEqual(t.matches, [])
        t.makeNextTurn()
        self.assertEqual(len(t.matches), (len(t.competitors) + 1) // 2)
        self.assertEqual(len([m for m in t.matches if m.competitor2 is None]), 1)
        self.assertEqual([m for m in t.matches if m.competitor2 is None][0].score1,
                         t.phantomscore)
        for m in t.matches:
            if m.turn==t.currentturn:
                m.score1 = randint(1, 25)
                m.score2 = 0
        t.makeNextTurn()
        self.assertEqual(len(t.matches), (len(t.competitors)+1))
        self.assertEqual(len([m for m in t.matches if m.competitor2 is None]), 2)
        try:
            t.makeNextTurn()
        except OperationAborted:
            pass
        else:
            assert False, "Should have raised an error!"

    def test_dazed_odd(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.dazedodd.description).one()
        self.assertEqual(t.matches, [])
        nboards = (len(t.competitors) + 1) // 2
        for turn in range(1, 4):
            t.makeNextTurn()
            self.assertEqual(len(t.matches), nboards*turn)
            self.assertEqual(len([m for m in t.matches if m.competitor2 is None]), turn)
            self.assertEqual([m for m in t.matches if m.competitor2 is None][0].score1,
                             t.phantomscore)
            for m in t.matches:
                if m.turn == t.currentturn and m.competitor2 is not None:
                    m.score1 = 10
                    m.score2 = 0
        try:
            t.makeNextTurn()
        except OperationAborted:
            pass
        else:
            assert False, "Should have raised an error!"

    def test_no_matches(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.odd.description).one()
        self.assertEqual(t.matches, [])
        # force update
        t.rankedturn = -1
        ranking = t.ranking
        self.assertEqual(len(ranking), len(t.competitors))

    def test_dazed_iterator(self):
        class C(object):
            def __init__(self, id, points):
                self.id = id
                self.points = points

        a = C('A', 10) # 0
        b = C('B', 10) # 1
        c = C('C', 10) # 2
        d = C('D', 10) # 3
        e = C('E', 10) # 4
        f = C('F', 9)  # 5
        g = C('G', 8)  # 6
        h = C('H', 8)  # 7

        ranking = [a, b, c, d, e, f, g, h]
        done = set([(a, f), (f, a),
                    (b, e), (e, b),
                    (c, d), (d, c),
                    (g, h), (h, g)])

        t = models.Tourney()
        order = t._dazedVisitor(a, ranking, done)
        order = list(order)
        expected = [2, 3, 4, 1, 6, 7]
        self.assertEqual(order, expected)

    def test_dazed_iterator_initial_even(self):
        class C(object):
            def __init__(self, id, points):
                self.id = id
                self.points = points

        a = C('A', 0) # 0
        b = C('B', 0) # 1
        c = C('C', 0) # 2
        d = C('D', 0) # 3
        e = C('E', 0) # 4
        f = C('F', 0) # 5
        g = C('G', 0) # 6
        h = C('H', 0) # 7

        ranking = [a, b, c, d, e, f, g, h]
        done = set()

        t = models.Tourney()
        order = t._dazedVisitor(a, ranking, done)
        order = list(order)
        expected = [4, 5, 6, 7, 1, 2, 3]
        self.assertEqual(order, expected)

    def test_dazed_iterator_initial_odd(self):
        class C(object):
            def __init__(self, id, points):
                self.id = id
                self.points = points

        a = C('A', 0) # 0
        b = C('B', 0) # 1
        c = C('C', 0) # 2
        d = C('D', 0) # 3
        e = C('E', 0) # 4
        f = C('F', 0) # 5
        g = C('G', 0) # 6

        ranking = [a, b, c, d, e, f, g]
        done = set()

        t = models.Tourney()
        order = t._dazedVisitor(a, ranking, done)
        order = list(order)
        expected = [3, 4, 5, 6, 1, 2]
        self.assertEqual(order, expected)

    def test_serial_iterator(self):
        class C(object):
            def __init__(self, id, points):
                self.id = id
                self.points = points

        a = C('A', 10) # 0
        b = C('B', 10) # 1
        c = C('C', 10) # 2
        d = C('D', 10) # 3
        e = C('E', 10) # 4
        f = C('F', 9)  # 5
        g = C('G', 8)  # 6
        h = C('H', 8)  # 7

        ranking = [a, b, c, d, e, f, g, h]
        done = set([(a, f), (f, a),
                    (b, e), (e, b),
                    (c, d), (d, c),
                    (g, h), (h, g)])

        t = models.Tourney()
        order = t._serialVisitor(a, ranking, done)
        order = list(order)
        expected = [1, 2, 3, 4, 6, 7]
        self.assertEqual(order, expected)

    def test_combine(self):
        models.tourney.logger.setLevel(logging.ERROR)
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.second.description).one()
        c = [1, 2, 3, 4, 5, 6]
        d = set()
        a = []
        n = t._combine(c, d)
        while n:
            a.append(n)
            for m in n:
                c1, c2 = m
                d.add((c1, c2))
                d.add((c2, c1))
            n = t._combine(c, d)
        self.assertEqual(len(a), 5)

    def test_asis_prizes(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.first.description).one()
        t.championship.prizes = 'asis'
        t.prized = False
        t.assignPrizes()
        s.flush()
        points = []
        prizes = []
        for c in t.ranking:
            points.append(c.points)
            prizes.append(c.prize)
        self.assertEqual(points, prizes)

    def test_fixed_prizes(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.first.description).one()
        t.championship.prizes = 'fixed'
        t.prized = False
        t.assignPrizes()
        s.flush()
        dates, cship = t.championship.championshipRanking()
        self.assertEqual(len(dates), len([st for st in t.championship.tourneys if st.prized]))
        self.assertEqual(len(cship), 6)
        self.assertEqual(cship[0][1], 18)

    def test_fixed40_prizes(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.first.description).one()
        t.championship.prizes = 'fixed40'
        t.prized = False
        t.assignPrizes()
        s.flush()
        r = t.ranking
        self.assertEqual(r[0].prize, 1000)
        self.assertEqual(r[1].prize, 900)
        self.assertEqual(r[2].prize, 800)
        self.assertEqual(r[3].prize, 750)

    def test_millesimal_prizes(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.third.description).one()
        t.championship.prizes = 'millesimal'
        t.prized = False
        t.assignPrizes()
        s.flush()
        dates, cship = t.championship.championshipRanking()
        self.assertEqual(len(dates), len([st for st in t.championship.tourneys if st.prized]))
        self.assertEqual(len(cship), len(t.competitors))
        r = t.ranking
        self.assertEqual(r[0].prize, 1000)
        self.assertEqual(r[1].prize, 750)
        self.assertEqual(r[2].prize, 500)
        self.assertEqual(r[3].prize, 250)

    def test_weighted_prizes(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.third.description).one()
        t.championship.prizes = 'weighted'
        t.prized = False
        t.assignPrizes()
        s.flush()
        r = t.ranking
        self.assertEqual(r[0].prize, 20)
        self.assertEqual(r[-1].prize, 5)

    def test_replay(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.third.description).one()
        d = TODAY+10*ONEDAY
        n = t.replay(d)
        s.flush()
        s = models.DBSession()
        n = s.query(models.Tourney) \
            .filter_by(idchampionship=t.idchampionship,
                       date=d).one()
        self.assertEqual(len(t.competitors), len(n.competitors))

    def test_replay_closed_championship(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.second.description).one()
        n = t.replay(TODAY)
        s.flush()
        self.assertEqual(n.championship.description, 'SCR 2010 (test)')

    def test_skip_worst_prize(self):
        s = models.DBSession()

        championship = s.query(models.Championship) \
            .filter_by(description=ChampionshipData.skipworstprize.description).one()
        dates, ranking = championship.championshipRanking()
        self.assertEqual(len(dates), 0)

        t1 = championship.tourneys[0]
        t1.assignPrizes()
        s.flush()

        dates, ranking = championship.championshipRanking()
        self.assertEqual(len(dates), 1)
        self.assertEqual(len(ranking), 2)

        first = ranking[0]
        players, prize, prizes, nprizes, skipped = first
        self.assertEqual(players[0].firstname, PlayerData.juri.firstname)
        self.assertEqual(skipped, None)

        d = TODAY
        t2 = t1.replay(d)
        t2.makeNextTurn()
        for m in t2.matches:
            m.score1 = 10
            m.score2 = 0
        s.flush()

        t2.assignPrizes()
        s.flush()

        dates, ranking = championship.championshipRanking()
        self.assertEqual(len(dates), 2)
        self.assertEqual(len(ranking), 2)

        first = ranking[0]
        players, prize, prizes, nprizes, skipped = first
        self.assertEqual(len(skipped), 1)


class TestTourneyResetPrizes(TestCase):
    def test_reset_prizes(self):
        from time import sleep

        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.first.description).one()
        modified = t.modified
        t.assignPrizes()
        s.flush()
        r = t.ranking
        self.assertEqual(r[0].prize, 18)
        self.assertEqual(r[-1].prize, 11)
        # Sleep for one second, to test the modified timestamp
        sleep(1)
        t.resetPrizes()
        s.flush()
        self.assertEqual(t.prized, False)
        r = t.ranking
        self.assertEqual(r[0].prize, 0)
        self.assertEqual(r[-1].prize, 0)
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.first.description).one()
        self.assertGreater(t.modified, modified)

    def test_reset_rated_tourney_prizes(self):
        with transaction.manager:
            s = models.DBSession()
            t = s.query(models.Tourney) \
                .filter_by(description=TourneyData.rated.description).one()
            oneplayerid = t.competitors[0].idplayer1
            t.assignPrizes()
            s.flush()
            t.resetPrizes()
            s.flush()

        s = models.DBSession()
        self.assertRaises(NoResultFound,
                          s.query(models.Rate)
                          .filter_by(idplayer=oneplayerid,
                                     date=TourneyData.rated.date).one)


class TestRetirement(TestCase):
    def test_retirement(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.first.description).one()
        t.prized = False
        comp = t.competitors[0]
        comp.retired = True
        lastturn = t.currentturn
        t.makeNextTurn()
        self.assertEqual(t.currentturn, lastturn+1)
        newmatches = [m for m in t.matches if m.turn == t.currentturn]
        assert [m for m in newmatches if not m.idcompetitor2]
        assert not [m for m in newmatches
                    if m.idcompetitor1 == comp.idcompetitor
                    or m.idcompetitor2 == comp.idcompetitor]


class TestUpdateDefault(TestCase):
    def test_update(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.first.description).one()

        result = t.update(dict(
            couplings='foo',
            location='bar',
            currentturn=1,
            prized=True
        ))

        self.assertEqual(result, dict(
            couplings=('serial', 'foo'),
            location=(None, 'bar'),
            currentturn=(3, 1),
            prized=(False, True)
        ))


class TestUpdateMissing(TestCase):
    def test_update(self):
        s = models.DBSession()
        t = s.query(models.Tourney) \
            .filter_by(description=TourneyData.first.description).one()

        result = t.update(dict(
            couplings='foo',
            location='bar',
            currentturn=1,
            prized=True
        ), missing_only=True)

        self.assertEqual(result, dict(
            location=(None, 'bar'),
            prized=(False, True)
        ))
