# coding: utf-8

import time
import copy

from anki.consts import STARTING_FACTOR
from tests.shared import getEmptyCol as _getEmptyCol
from anki.utils import  intTime
from anki.hooks import addHook

def getEmptyCol():
    return _getEmptyCol(schedVer=2)

def test_clock():
    d = getEmptyCol()
    if (d.sched.dayCutoff - intTime()) < 10*60:
        raise Exception("Unit tests will fail around the day rollover.")

def checkRevIvl(d, c, targetIvl):
    min, max = d.sched._fuzzIvlRange(targetIvl)
    return min <= c.ivl <= max

def test_basics():
    d = getEmptyCol()
    d.reset()
    assert not d.sched.getCard()

def test_new():
    d = getEmptyCol()
    d.reset()
    assert d.sched.newCount == 0
    # add a note
    f = d.newNote()
    f['Front'] = "one"; f['Back'] = "two"
    d.addNote(f)
    d.reset()
    assert d.sched.newCount == 1
    # fetch it
    c = d.sched.getCard()
    assert c
    assert c.queue == 0
    assert c.type == 0
    # if we answer it, it should become a learn card
    t = intTime()
    d.sched.answerCard(c, 1)
    assert c.queue == 1
    assert c.type == 1
    assert c.due >= t

    # disabled for now, as the learn fudging makes this randomly fail
    # # the default order should ensure siblings are not seen together, and
    # # should show all cards
    # m = d.models.current(); mm = d.models
    # t = mm.newTemplate("Reverse")
    # t['qfmt'] = "{{Back}}"
    # t['afmt'] = "{{Front}}"
    # mm.addTemplate(m, t)
    # mm.save(m)
    # f = d.newNote()
    # f['Front'] = u"2"; f['Back'] = u"2"
    # d.addNote(f)
    # f = d.newNote()
    # f['Front'] = u"3"; f['Back'] = u"3"
    # d.addNote(f)
    # d.reset()
    # qs = ("2", "3", "2", "3")
    # for n in range(4):
    #     c = d.sched.getCard()
    #     assert qs[n] in c.q()
    #     d.sched.answerCard(c, 2)

def test_newLimits():
    d = getEmptyCol()
    # add some notes
    g2 = d.decks.id("Default::foo")
    for i in range(30):
        f = d.newNote()
        f['Front'] = str(i)
        if i > 4:
            f.model()['did'] = g2
        d.addNote(f)
    # give the child deck a different configuration
    c2 = d.decks.confId("new conf")
    d.decks.setConf(d.decks.get(g2), c2)
    d.reset()
    # both confs have defaulted to a limit of 20
    assert d.sched.newCount == 20
    # first card we get comes from parent
    c = d.sched.getCard()
    assert c.did == 1
    # limit the parent to 10 cards, meaning we get 10 in total
    conf1 = d.decks.confForDid(1)
    conf1['new']['perDay'] = 10
    d.reset()
    assert d.sched.newCount == 10
    # if we limit child to 4, we should get 9
    conf2 = d.decks.confForDid(g2)
    conf2['new']['perDay'] = 4
    d.reset()
    assert d.sched.newCount == 9

def test_newBoxes():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    d.reset()
    c = d.sched.getCard()
    d.sched._cardConf(c)['new']['delays'] = [1,2,3,4,5]
    d.sched.answerCard(c, 2)
    # should handle gracefully
    d.sched._cardConf(c)['new']['delays'] = [1]
    d.sched.answerCard(c, 2)

def test_learn():
    d = getEmptyCol()
    # add a note
    f = d.newNote()
    f['Front'] = "one"; f['Back'] = "two"
    f = d.addNote(f)
    # set as a learn card and rebuild queues
    d.db.execute("update cards set queue=0, type=0")
    d.reset()
    # sched.getCard should return it, since it's due in the past
    c = d.sched.getCard()
    assert c
    d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10]
    # fail it
    d.sched.answerCard(c, 1)
    # it should have three reps left to graduation
    assert c.left%1000 == 3
    assert c.left//1000 == 3
    # it should by due in 30 seconds
    t = round(c.due - time.time())
    assert t >= 25 and t <= 40
    # pass it once
    d.sched.answerCard(c, 3)
    # it should by due in 3 minutes
    dueIn = c.due - time.time()
    assert 179 <= dueIn <= 180*1.25
    assert c.left%1000 == 2
    assert c.left//1000 == 2
    # check log is accurate
    log = d.db.first("select * from revlog order by id desc")
    assert log[3] == 3
    assert log[4] == -180
    assert log[5] == -30
    # pass again
    d.sched.answerCard(c, 3)
    # it should by due in 10 minutes
    dueIn = c.due - time.time()
    assert 599 <= dueIn <= 600*1.25
    assert c.left%1000 == 1
    assert c.left//1000 == 1
    # the next pass should graduate the card
    assert c.queue == 1
    assert c.type == 1
    d.sched.answerCard(c, 3)
    assert c.queue == 2
    assert c.type == 2
    # should be due tomorrow, with an interval of 1
    assert c.due == d.sched.today+1
    assert c.ivl == 1
    # or normal removal
    c.type = 0
    c.queue = 1
    d.sched.answerCard(c, 4)
    assert c.type == 2
    assert c.queue == 2
    assert checkRevIvl(d, c, 4)
    # revlog should have been updated each time
    assert d.db.scalar("select count() from revlog where type = 0") == 5

def test_relearn():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    c = f.cards()[0]
    c.ivl = 100
    c.due = d.sched.today
    c.type = c.queue = 2
    c.flush()

    # fail the card
    d.reset()
    c = d.sched.getCard()
    d.sched.answerCard(c, 1)
    assert c.queue == 1
    assert c.type == 3
    assert c.ivl == 1

    # immediately graduate it
    d.sched.answerCard(c, 4)
    assert c.queue == c.type == 2
    assert c.ivl == 1
    assert c.due == d.sched.today + c.ivl

def test_relearn_no_steps():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    c = f.cards()[0]
    c.ivl = 100
    c.due = d.sched.today
    c.type = c.queue = 2
    c.flush()

    conf = d.decks.confForDid(1)
    conf['lapse']['delays'] = []
    d.decks.save(conf)

    # fail the card
    d.reset()
    c = d.sched.getCard()
    d.sched.answerCard(c, 1)
    assert c.type == c.queue == 2

def test_learn_collapsed():
    d = getEmptyCol()
    # add 2 notes
    f = d.newNote()
    f['Front'] = "1"
    f = d.addNote(f)
    f = d.newNote()
    f['Front'] = "2"
    f = d.addNote(f)
    # set as a learn card and rebuild queues
    d.db.execute("update cards set queue=0, type=0")
    d.reset()
    # should get '1' first
    c = d.sched.getCard()
    assert c.q().endswith("1")
    # pass it so it's due in 10 minutes
    d.sched.answerCard(c, 3)
    # get the other card
    c = d.sched.getCard()
    assert c.q().endswith("2")
    # fail it so it's due in 1 minute
    d.sched.answerCard(c, 1)
    # we shouldn't get the same card again
    c = d.sched.getCard()
    assert not c.q().endswith("2")

def test_learn_day():
    d = getEmptyCol()
    # add a note
    f = d.newNote()
    f['Front'] = "one"
    f = d.addNote(f)
    d.sched.reset()
    c = d.sched.getCard()
    d.sched._cardConf(c)['new']['delays'] = [1, 10, 1440, 2880]
    # pass it
    d.sched.answerCard(c, 3)
    # two reps to graduate, 1 more today
    assert c.left%1000 == 3
    assert c.left//1000 == 1
    assert d.sched.counts() == (0, 1, 0)
    c = d.sched.getCard()
    ni = d.sched.nextIvl
    assert ni(c, 3) == 86400
    # answering it will place it in queue 3
    d.sched.answerCard(c, 3)
    assert c.due == d.sched.today+1
    assert c.queue == 3
    assert not d.sched.getCard()
    # for testing, move it back a day
    c.due -= 1
    c.flush()
    d.reset()
    assert d.sched.counts() == (0, 1, 0)
    c = d.sched.getCard()
    # nextIvl should work
    assert ni(c, 3) == 86400*2
    # if we fail it, it should be back in the correct queue
    d.sched.answerCard(c, 1)
    assert c.queue == 1
    d.undo()
    d.reset()
    c = d.sched.getCard()
    d.sched.answerCard(c, 3)
    # simulate the passing of another two days
    c.due -= 2
    c.flush()
    d.reset()
    # the last pass should graduate it into a review card
    assert ni(c, 3) == 86400
    d.sched.answerCard(c, 3)
    assert c.queue == c.type == 2
    # if the lapse step is tomorrow, failing it should handle the counts
    # correctly
    c.due = 0
    c.flush()
    d.reset()
    assert d.sched.counts() == (0, 0, 1)
    d.sched._cardConf(c)['lapse']['delays'] = [1440]
    c = d.sched.getCard()
    d.sched.answerCard(c, 1)
    assert c.queue == 3
    assert d.sched.counts() == (0, 0, 0)

def test_reviews():
    d = getEmptyCol()
    # add a note
    f = d.newNote()
    f['Front'] = "one"; f['Back'] = "two"
    d.addNote(f)
    # set the card up as a review card, due 8 days ago
    c = f.cards()[0]
    c.type = 2
    c.queue = 2
    c.due = d.sched.today - 8
    c.factor = STARTING_FACTOR
    c.reps = 3
    c.lapses = 1
    c.ivl = 100
    c.startTimer()
    c.flush()
    # save it for later use as well
    cardcopy = copy.copy(c)
    # try with an ease of 2
    ##################################################
    c = copy.copy(cardcopy)
    c.flush()
    d.reset()
    d.sched.answerCard(c, 2)
    assert c.queue == 2
    # the new interval should be (100) * 1.2 = 120
    assert checkRevIvl(d, c, 120)
    assert c.due == d.sched.today + c.ivl
    # factor should have been decremented
    assert c.factor == 2350
    # check counters
    assert c.lapses == 1
    assert c.reps == 4
    # ease 3
    ##################################################
    c = copy.copy(cardcopy)
    c.flush()
    d.sched.answerCard(c, 3)
    # the new interval should be (100 + 8/2) * 2.5 = 260
    assert checkRevIvl(d, c, 260)
    assert c.due == d.sched.today + c.ivl
    # factor should have been left alone
    assert c.factor == STARTING_FACTOR
    # ease 4
    ##################################################
    c = copy.copy(cardcopy)
    c.flush()
    d.sched.answerCard(c, 4)
    # the new interval should be (100 + 8) * 2.5 * 1.3 = 351
    assert checkRevIvl(d, c, 351)
    assert c.due == d.sched.today + c.ivl
    # factor should have been increased
    assert c.factor == 2650
    # leech handling
    ##################################################
    c = copy.copy(cardcopy)
    c.lapses = 7
    c.flush()
    # steup hook
    hooked = []
    def onLeech(card):
        hooked.append(1)
    addHook("leech", onLeech)
    d.sched.answerCard(c, 1)
    assert hooked
    assert c.queue == -1
    c.load()
    assert c.queue == -1

def test_review_limits():
    d = getEmptyCol()

    parent = d.decks.get(d.decks.id("parent"))
    child = d.decks.get(d.decks.id("parent::child"))

    pconf = d.decks.getConf(d.decks.confId("parentConf"))
    cconf = d.decks.getConf(d.decks.confId("childConf"))

    pconf['rev']['perDay'] = 5
    d.decks.updateConf(pconf)
    d.decks.setConf(parent, pconf['id'])
    cconf['rev']['perDay'] = 10
    d.decks.updateConf(cconf)
    d.decks.setConf(child, cconf['id'])

    m = d.models.current()
    m['did'] = child['id']
    d.models.save(m)

    # add some cards
    for i in range(20):
        f = d.newNote()
        f['Front'] = "one"; f['Back'] = "two"
        d.addNote(f)

        # make them reviews
        c = f.cards()[0]
        c.queue = c.type = 2
        c.due = 0
        c.flush()

    tree = d.sched.deckDueTree()
    # (('Default', 1, 0, 0, 0, ()), ('parent', 1514457677462, 5, 0, 0, (('child', 1514457677463, 5, 0, 0, ()),)))
    assert tree[1][2] == 5 # parent
    assert tree[1][5][0][2] == 5 # child

    # .counts() should match
    d.decks.select(child['id'])
    d.sched.reset()
    assert d.sched.counts() == (0, 0, 5)

    # answering a card in the child should decrement parent count
    c = d.sched.getCard()
    d.sched.answerCard(c, 3)
    assert d.sched.counts() == (0, 0, 4)

    tree = d.sched.deckDueTree()
    assert tree[1][2] == 4 # parent
    assert tree[1][5][0][2] == 4 # child

    # switch limits
    d.decks.setConf(parent, cconf['id'])
    d.decks.setConf(child, pconf['id'])
    d.decks.select(parent['id'])
    d.sched.reset()

    # child limits do not affect the parent
    tree = d.sched.deckDueTree()
    assert tree[1][2] == 9 # parent
    assert tree[1][5][0][2] == 4 # child

def test_button_spacing():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    # 1 day ivl review card due now
    c = f.cards()[0]
    c.type = 2
    c.queue = 2
    c.due = d.sched.today
    c.reps = 1
    c.ivl = 1
    c.startTimer()
    c.flush()
    d.reset()
    ni = d.sched.nextIvlStr
    assert ni(c, 2) == "2 days"
    assert ni(c, 3) == "3 days"
    assert ni(c, 4) == "4 days"

    # if hard factor is <= 1, then hard may not increase
    conf = d.decks.confForDid(1)
    conf['rev']['hardFactor'] = 1
    assert ni(c, 2) == "1 day"

def test_overdue_lapse():
    # disabled in commit 3069729776990980f34c25be66410e947e9d51a2
    return
    d = getEmptyCol()
    # add a note
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    # simulate a review that was lapsed and is now due for its normal review
    c = f.cards()[0]
    c.type = 2
    c.queue = 1
    c.due = -1
    c.odue = -1
    c.factor = STARTING_FACTOR
    c.left = 2002
    c.ivl = 0
    c.flush()
    d.sched._clearOverdue = False
    # checkpoint
    d.save()
    d.sched.reset()
    assert d.sched.counts() == (0, 2, 0)
    c = d.sched.getCard()
    d.sched.answerCard(c, 3)
    # it should be due tomorrow
    assert c.due == d.sched.today + 1
    # revert to before
    d.rollback()
    d.sched._clearOverdue = True
    # with the default settings, the overdue card should be removed from the
    # learning queue
    d.sched.reset()
    assert d.sched.counts() == (0, 0, 1)

def test_finished():
    d = getEmptyCol()
    # nothing due
    assert "Congratulations" in d.sched.finishedMsg()
    assert "limit" not in d.sched.finishedMsg()
    f = d.newNote()
    f['Front'] = "one"; f['Back'] = "two"
    d.addNote(f)
    # have a new card
    assert "new cards available" in d.sched.finishedMsg()
    # turn it into a review
    d.reset()
    c = f.cards()[0]
    c.startTimer()
    d.sched.answerCard(c, 3)
    # nothing should be due tomorrow, as it's due in a week
    assert "Congratulations" in d.sched.finishedMsg()
    assert "limit" not in d.sched.finishedMsg()

def test_nextIvl():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"; f['Back'] = "two"
    d.addNote(f)
    d.reset()
    conf = d.decks.confForDid(1)
    conf['new']['delays'] = [0.5, 3, 10]
    conf['lapse']['delays'] = [1, 5, 9]
    c = d.sched.getCard()
    # new cards
    ##################################################
    ni = d.sched.nextIvl
    assert ni(c, 1) == 30
    assert ni(c, 2) == (30+180)//2
    assert ni(c, 3) == 180
    assert ni(c, 4) == 4*86400
    d.sched.answerCard(c, 1)
    # cards in learning
    ##################################################
    assert ni(c, 1) == 30
    assert ni(c, 2) == (30+180)//2
    assert ni(c, 3) == 180
    assert ni(c, 4) == 4*86400
    d.sched.answerCard(c, 3)
    assert ni(c, 1) == 30
    assert ni(c, 2) == (180+600)//2
    assert ni(c, 3) == 600
    assert ni(c, 4) == 4*86400
    d.sched.answerCard(c, 3)
    # normal graduation is tomorrow
    assert ni(c, 3) == 1*86400
    assert ni(c, 4) == 4*86400
    # lapsed cards
    ##################################################
    c.type = 2
    c.ivl = 100
    c.factor = STARTING_FACTOR
    assert ni(c, 1) == 60
    assert ni(c, 3) == 100*86400
    assert ni(c, 4) == 100*86400
    # review cards
    ##################################################
    c.queue = 2
    c.ivl = 100
    c.factor = STARTING_FACTOR
    # failing it should put it at 60s
    assert ni(c, 1) == 60
    # or 1 day if relearn is false
    d.sched._cardConf(c)['lapse']['delays']=[]
    assert ni(c, 1) == 1*86400
    # (* 100 1.2 86400)10368000.0
    assert ni(c, 2) == 10368000
    # (* 100 2.5 86400)21600000.0
    assert ni(c, 3) == 21600000
    # (* 100 2.5 1.3 86400)28080000.0
    assert ni(c, 4) == 28080000
    assert d.sched.nextIvlStr(c, 4) == "10.8 months"

def test_bury():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    c = f.cards()[0]
    f = d.newNote()
    f['Front'] = "two"
    d.addNote(f)
    c2 = f.cards()[0]
    # burying
    d.sched.buryCards([c.id], manual=True)
    c.load()
    assert c.queue == -3
    d.sched.buryCards([c2.id], manual=False)
    c2.load()
    assert c2.queue == -2

    d.reset()
    assert not d.sched.getCard()

    d.sched.unburyCardsForDeck(type="manual")
    c.load(); assert c.queue == 0
    c2.load(); assert c2.queue == -2

    d.sched.unburyCardsForDeck(type="siblings")
    c2.load(); assert c2.queue == 0

    d.sched.buryCards([c.id, c2.id])
    d.sched.unburyCardsForDeck(type="all")

    d.reset()

    assert d.sched.counts() == (2, 0, 0)

def test_suspend():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    c = f.cards()[0]
    # suspending
    d.reset()
    assert d.sched.getCard()
    d.sched.suspendCards([c.id])
    d.reset()
    assert not d.sched.getCard()
    # unsuspending
    d.sched.unsuspendCards([c.id])
    d.reset()
    assert d.sched.getCard()
    # should cope with rev cards being relearnt
    c.due = 0; c.ivl = 100; c.type = 2; c.queue = 2; c.flush()
    d.reset()
    c = d.sched.getCard()
    d.sched.answerCard(c, 1)
    assert c.due >= time.time()
    due = c.due
    assert c.queue == 1
    assert c.type == 3
    d.sched.suspendCards([c.id])
    d.sched.unsuspendCards([c.id])
    c.load()
    assert c.queue == 1
    assert c.type == 3
    assert c.due == due
    # should cope with cards in cram decks
    c.due = 1
    c.flush()
    cram = d.decks.newDyn("tmp")
    d.sched.rebuildDyn()
    c.load()
    assert c.due != 1
    assert c.did != 1
    d.sched.suspendCards([c.id])
    c.load()
    assert c.due != 1
    assert c.did != 1
    assert c.odue == 1

def test_filt_reviewing_early_normal():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    c = f.cards()[0]
    c.ivl = 100
    c.type = c.queue = 2
    # due in 25 days, so it's been waiting 75 days
    c.due = d.sched.today + 25
    c.mod = 1
    c.factor = STARTING_FACTOR
    c.startTimer()
    c.flush()
    d.reset()
    assert d.sched.counts() == (0,0,0)
    # create a dynamic deck and refresh it
    did = d.decks.newDyn("Cram")
    d.sched.rebuildDyn(did)
    d.reset()
    # should appear as normal in the deck list
    assert sorted(d.sched.deckDueList())[0][2] == 1
    # and should appear in the counts
    assert d.sched.counts() == (0,0,1)
    # grab it and check estimates
    c = d.sched.getCard()
    assert d.sched.answerButtons(c) == 4
    assert d.sched.nextIvl(c, 1) == 600
    assert d.sched.nextIvl(c, 2) == int(75*1.2)*86400
    assert d.sched.nextIvl(c, 3) == int(75*2.5)*86400
    assert d.sched.nextIvl(c, 4) == int(75*2.5*1.15)*86400

    # answer 'good'
    d.sched.answerCard(c, 3)
    checkRevIvl(d, c, 90)
    assert c.due == d.sched.today + c.ivl
    assert not c.odue
    # should not be in learning
    assert c.queue == 2
    # should be logged as a cram rep
    assert d.db.scalar(
        "select type from revlog order by id desc limit 1") == 3

    # due in 75 days, so it's been waiting 25 days
    c.ivl = 100
    c.due = d.sched.today + 75
    c.flush()
    d.sched.rebuildDyn(did)
    d.reset()
    c = d.sched.getCard()

    assert d.sched.nextIvl(c, 2) == 60*86400
    assert d.sched.nextIvl(c, 3) == 100*86400
    assert d.sched.nextIvl(c, 4) == 114*86400

def test_filt_keep_lrn_state():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)

    # fail the card outside filtered deck
    c = d.sched.getCard()
    d.sched.answerCard(c, 1)

    assert c.type == c.queue == 1

    # create a dynamic deck and refresh it
    did = d.decks.newDyn("Cram")
    d.sched.rebuildDyn(did)
    d.reset()

    # card should still be in learning state
    c.load()
    assert c.type == c.queue == 1

    # emptying the deck preserves learning state
    d.sched.emptyDyn(did)
    c.load()
    assert c.type == c.queue == 1

def test_preview():
    # add cards
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    c = f.cards()[0]
    orig = copy.copy(c)
    f2 = d.newNote()
    f2['Front'] = "two"
    d.addNote(f2)
    # cram deck
    did = d.decks.newDyn("Cram")
    cram = d.decks.get(did)
    cram['resched'] = False
    d.sched.rebuildDyn(did)
    d.reset()
    # grab the first card
    c = d.sched.getCard()
    assert d.sched.answerButtons(c) == 2
    assert d.sched.nextIvl(c, 1) == 600
    assert d.sched.nextIvl(c, 2) == 0
    # failing it will push its due time back
    due = c.due
    d.sched.answerCard(c, 1)
    assert c.due != due

    # the other card should come next
    c2 = d.sched.getCard()
    assert c2.id != c.id

    # passing it will remove it
    d.sched.answerCard(c2, 2)
    assert c2.queue == 0
    assert c2.reps == 0
    assert c2.type == 0

    # the other card should appear again
    c = d.sched.getCard()
    assert c.id == orig.id

    # emptying the filtered deck should restore card
    d.sched.emptyDyn(did)
    c.load()
    assert c.queue == 0
    assert c.reps == 0
    assert c.type == 0

def test_ordcycle():
    d = getEmptyCol()
    # add two more templates and set second active
    m = d.models.current(); mm = d.models
    t = mm.newTemplate("Reverse")
    t['qfmt'] = "{{Back}}"
    t['afmt'] = "{{Front}}"
    mm.addTemplate(m, t)
    t = mm.newTemplate("f2")
    t['qfmt'] = "{{Front}}"
    t['afmt'] = "{{Back}}"
    mm.addTemplate(m, t)
    mm.save(m)
    # create a new note; it should have 3 cards
    f = d.newNote()
    f['Front'] = "1"; f['Back'] = "1"
    d.addNote(f)
    assert d.cardCount() == 3
    d.reset()
    # ordinals should arrive in order
    assert d.sched.getCard().ord == 0
    assert d.sched.getCard().ord == 1
    assert d.sched.getCard().ord == 2

def test_counts_idx():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"; f['Back'] = "two"
    d.addNote(f)
    d.reset()
    assert d.sched.counts() == (1, 0, 0)
    c = d.sched.getCard()
    # counter's been decremented but idx indicates 1
    assert d.sched.counts() == (0, 0, 0)
    assert d.sched.countIdx(c) == 0
    # answer to move to learn queue
    d.sched.answerCard(c, 1)
    assert d.sched.counts() == (0, 1, 0)
    # fetching again will decrement the count
    c = d.sched.getCard()
    assert d.sched.counts() == (0, 0, 0)
    assert d.sched.countIdx(c) == 1
    # answering should add it back again
    d.sched.answerCard(c, 1)
    assert d.sched.counts() == (0, 1, 0)

def test_repCounts():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    d.reset()
    # lrnReps should be accurate on pass/fail
    assert d.sched.counts() == (1, 0, 0)
    d.sched.answerCard(d.sched.getCard(), 1)
    assert d.sched.counts() == (0, 1, 0)
    d.sched.answerCard(d.sched.getCard(), 1)
    assert d.sched.counts() == (0, 1, 0)
    d.sched.answerCard(d.sched.getCard(), 3)
    assert d.sched.counts() == (0, 1, 0)
    d.sched.answerCard(d.sched.getCard(), 1)
    assert d.sched.counts() == (0, 1, 0)
    d.sched.answerCard(d.sched.getCard(), 3)
    assert d.sched.counts() == (0, 1, 0)
    d.sched.answerCard(d.sched.getCard(), 3)
    assert d.sched.counts() == (0, 0, 0)
    f = d.newNote()
    f['Front'] = "two"
    d.addNote(f)
    d.reset()
    # initial pass should be correct too
    d.sched.answerCard(d.sched.getCard(), 3)
    assert d.sched.counts() == (0, 1, 0)
    d.sched.answerCard(d.sched.getCard(), 1)
    assert d.sched.counts() == (0, 1, 0)
    d.sched.answerCard(d.sched.getCard(), 4)
    assert d.sched.counts() == (0, 0, 0)
    # immediate graduate should work
    f = d.newNote()
    f['Front'] = "three"
    d.addNote(f)
    d.reset()
    d.sched.answerCard(d.sched.getCard(), 4)
    assert d.sched.counts() == (0, 0, 0)
    # and failing a review should too
    f = d.newNote()
    f['Front'] = "three"
    d.addNote(f)
    c = f.cards()[0]
    c.type = 2
    c.queue = 2
    c.due = d.sched.today
    c.flush()
    d.reset()
    assert d.sched.counts() == (0, 0, 1)
    d.sched.answerCard(d.sched.getCard(), 1)
    assert d.sched.counts() == (0, 1, 0)

def test_timing():
    d = getEmptyCol()
    # add a few review cards, due today
    for i in range(5):
        f = d.newNote()
        f['Front'] = "num"+str(i)
        d.addNote(f)
        c = f.cards()[0]
        c.type = 2
        c.queue = 2
        c.due = 0
        c.flush()
    # fail the first one
    d.reset()
    c = d.sched.getCard()
    d.sched.answerCard(c, 1)
    # the next card should be another review
    c2 = d.sched.getCard()
    assert c2.queue == 2
    # if the failed card becomes due, it should show first
    c.due = time.time() - 1
    c.flush()
    d.reset()
    c = d.sched.getCard()
    assert c.queue == 1

def test_collapse():
    d = getEmptyCol()
    # add a note
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    d.reset()
    # test collapsing
    c = d.sched.getCard()
    d.sched.answerCard(c, 1)
    c = d.sched.getCard()
    d.sched.answerCard(c, 4)
    assert not d.sched.getCard()

def test_deckDue():
    d = getEmptyCol()
    # add a note with default deck
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    # and one that's a child
    f = d.newNote()
    f['Front'] = "two"
    default1 = f.model()['did'] = d.decks.id("Default::1")
    d.addNote(f)
    # make it a review card
    c = f.cards()[0]
    c.queue = 2
    c.due = 0
    c.flush()
    # add one more with a new deck
    f = d.newNote()
    f['Front'] = "two"
    foobar = f.model()['did'] = d.decks.id("foo::bar")
    d.addNote(f)
    # and one that's a sibling
    f = d.newNote()
    f['Front'] = "three"
    foobaz = f.model()['did'] = d.decks.id("foo::baz")
    d.addNote(f)
    d.reset()
    assert len(d.decks.decks) == 5
    cnts = d.sched.deckDueList()
    assert cnts[0] == ["Default", 1, 1, 0, 1]
    assert cnts[1] == ["Default::1", default1, 1, 0, 0]
    assert cnts[2] == ["foo", d.decks.id("foo"), 0, 0, 0]
    assert cnts[3] == ["foo::bar", foobar, 0, 0, 1]
    assert cnts[4] == ["foo::baz", foobaz, 0, 0, 1]
    tree = d.sched.deckDueTree()
    assert tree[0][0] == "Default"
    # sum of child and parent
    assert tree[0][1] == 1
    assert tree[0][2] == 1
    assert tree[0][4] == 1
    # child count is just review
    assert tree[0][5][0][0] == "1"
    assert tree[0][5][0][1] == default1
    assert tree[0][5][0][2] == 1
    assert tree[0][5][0][4] == 0
    # code should not fail if a card has an invalid deck
    c.did = 12345; c.flush()
    d.sched.deckDueList()
    d.sched.deckDueTree()

def test_deckTree():
    d = getEmptyCol()
    d.decks.id("new::b::c")
    d.decks.id("new2")
    # new should not appear twice in tree
    names = [x[0] for x in d.sched.deckDueTree()]
    names.remove("new")
    assert "new" not in names

def test_deckFlow():
    d = getEmptyCol()
    # add a note with default deck
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    # and one that's a child
    f = d.newNote()
    f['Front'] = "two"
    default1 = f.model()['did'] = d.decks.id("Default::2")
    d.addNote(f)
    # and another that's higher up
    f = d.newNote()
    f['Front'] = "three"
    default1 = f.model()['did'] = d.decks.id("Default::1")
    d.addNote(f)
    # should get top level one first, then ::1, then ::2
    d.reset()
    assert d.sched.counts() == (3,0,0)
    for i in "one", "three", "two":
        c = d.sched.getCard()
        assert c.note()['Front'] == i
        d.sched.answerCard(c, 3)

def test_reorder():
    d = getEmptyCol()
    # add a note with default deck
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    f2 = d.newNote()
    f2['Front'] = "two"
    d.addNote(f2)
    assert f2.cards()[0].due == 2
    found=False
    # 50/50 chance of being reordered
    for i in range(20):
        d.sched.randomizeCards(1)
        if f.cards()[0].due != f.id:
            found=True
            break
    assert found
    d.sched.orderCards(1)
    assert f.cards()[0].due == 1
    # shifting
    f3 = d.newNote()
    f3['Front'] = "three"
    d.addNote(f3)
    f4 = d.newNote()
    f4['Front'] = "four"
    d.addNote(f4)
    assert f.cards()[0].due == 1
    assert f2.cards()[0].due == 2
    assert f3.cards()[0].due == 3
    assert f4.cards()[0].due == 4
    d.sched.sortCards([
        f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
    assert f.cards()[0].due == 3
    assert f2.cards()[0].due == 4
    assert f3.cards()[0].due == 1
    assert f4.cards()[0].due == 2

def test_forget():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    c = f.cards()[0]
    c.queue = 2; c.type = 2; c.ivl = 100; c.due = 0
    c.flush()
    d.reset()
    assert d.sched.counts() == (0, 0, 1)
    d.sched.forgetCards([c.id])
    d.reset()
    assert d.sched.counts() == (1, 0, 0)

def test_resched():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    c = f.cards()[0]
    d.sched.reschedCards([c.id], 0, 0)
    c.load()
    assert c.due == d.sched.today
    assert c.ivl == 1
    assert c.queue == c.type == 2
    d.sched.reschedCards([c.id], 1, 1)
    c.load()
    assert c.due == d.sched.today+1
    assert c.ivl == +1

def test_norelearn():
    d = getEmptyCol()
    # add a note
    f = d.newNote()
    f['Front'] = "one"
    d.addNote(f)
    c = f.cards()[0]
    c.type = 2
    c.queue = 2
    c.due = 0
    c.factor = STARTING_FACTOR
    c.reps = 3
    c.lapses = 1
    c.ivl = 100
    c.startTimer()
    c.flush()
    d.reset()
    d.sched.answerCard(c, 1)
    d.sched._cardConf(c)['lapse']['delays'] = []
    d.sched.answerCard(c, 1)

def test_failmult():
    d = getEmptyCol()
    f = d.newNote()
    f['Front'] = "one"; f['Back'] = "two"
    d.addNote(f)
    c = f.cards()[0]
    c.type = 2
    c.queue = 2
    c.ivl = 100
    c.due = d.sched.today - c.ivl
    c.factor = STARTING_FACTOR
    c.reps = 3
    c.lapses = 1
    c.startTimer()
    c.flush()
    d.sched._cardConf(c)['lapse']['mult'] = 0.5
    c = d.sched.getCard()
    d.sched.answerCard(c, 1)
    assert c.ivl == 50
    d.sched.answerCard(c, 1)
    assert c.ivl == 25

def test_moveVersions():
    col = _getEmptyCol(schedVer=1)

    n = col.newNote()
    n['Front'] = "one"
    col.addNote(n)

    # make it a learning card
    col.reset()
    c = col.sched.getCard()
    col.sched.answerCard(c, 1)

    # the move to v2 should reset it to new
    col.changeSchedulerVer(2)
    c.load()
    assert c.queue == 0
    assert c.type == 0

    # fail it again, and manually bury it
    col.reset()
    c = col.sched.getCard()
    col.sched.answerCard(c, 1)
    col.sched.buryCards([c.id])
    c.load()
    assert c.queue == -3

    # revert to version 1
    col.changeSchedulerVer(1)

    # card should have moved queues
    c.load()
    assert c.queue == -2

    # and it should be new again when unburied
    col.sched.unburyCards()
    c.load()
    assert c.queue == c.type == 0
