local G=require("\109\111\115\101\115")local z=(require("\115\117\112\112\111\114\116\046\102\097\099\116\111\114\121")).create local f=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\109\098\111\115\046\067\111\109\098\111")local u=require("\105\110\118\111\107\097\116\105\111\110\046\100\111\116\097\050\046\116\097\108\101\110\116\115")local p=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\109\098\111\115\046\066\097\115\101\067\111\109\098\111")local q=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\109\098\111\115\046\067\111\109\098\111\083\101\113\117\101\110\099\101")local c=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\110\115\116\046\105\110\118\111\107\101\114")local w={id=13,specialty="\113\119",stance="\100\101\102\101\110\115\105\118\101";heroLevel=25,damageRating=5,difficultyRating=5,gold=1234;tags={"\108\097\116\101-\103\097\109\101"};items={"\105\116\101\109\095\098\108\105\110\107"};orbs={7,7,7};talents=u.Select(u.L10_LEFT,u.L15_RIGHT,u.L20_RIGHT,u.L25_RIGHT),sequence={{id=1;name="\115\116\101\112\049",required=true,next={2,3}};{id=2;name="\115\116\101\112\050";next={3}};{id=3,name="\115\116\101\112\051";required=true}}}describe("\067\111\109\098\111",function()local u,g local function Q()return 10 end local T=G.property("\105\100")local function j(z)return G.map(z,T)end local s=z("\097\098\105\108\105\116\121",{name="\115\116\101\112\049",AbilityDuration=.5})local M=z("\097\098\105\108\105\116\121",{name="\115\116\101\112\050";AbilityDuration=.3})local X=z("\097\098\105\108\105\116\121",{name="\115\116\101\112\051",AbilityDuration=.25})before_each(function()g=spy.new(Q)u=f(w,{clock=g})end)it("\105\110\104\101\114\105\116\115 \102\114\111\109 \066\097\115\101\067\111\109\098\111",function()assert.is_true(p:class_of(u))end)describe("\099\111\110\115\116\114\117\099\116\111\114",function()it("\105\110\105\116\105\097\108\105\122\101\115 \097\116\116\114\105\098\117\116\101\115",function()for G,z in pairs(G.omit(w,{"\115\101\113\117\101\110\099\101"}))do assert.same(w[G],z)end assert.is_false(u.started)assert.is_false(u.failed)assert.is_false(u.finished)assert.equal(0,u.count)assert.equal(0,u.damage)assert.equal(g,u.clock)assert.is_true(q:class_of(u.sequence))end)end)describe("\035\116\111\100\111\116",function()it("\100\101\108\101\103\097\116\101\115 \116\111 \115\101\113\117\101\110\099\101",function()local G=spy.on(u.sequence,"\116\111\100\111\116")u:todot();(assert.spy(G)).was_called(1)end)end)describe("\035\067\117\114\114\101\110\116\083\116\101\112\073\100",function()it("\114\101\116\117\114\110\115 \116\104\101 \099\117\114\114\101\110\116 \115\116\101\112 \105\100",function()assert.is_nil(u:CurrentStepId())assert.is_true(u:Progress(s))assert.equal(1,u:CurrentStepId())assert.is_true(u:Progress(M))assert.equal(2,u:CurrentStepId())assert.is_true(u:Progress(X))assert.equal(3,u:CurrentStepId())assert.is_true(u:PreFinish())assert.is_nil(u:CurrentStepId())assert.is_true(u:Finish())assert.is_nil(u:CurrentStepId())end)end)describe("\035\067\117\114\114\101\110\116\083\116\101\112",function()it("\114\101\116\117\114\110\115 \116\104\101 \099\117\114\114\101\110\116 \115\116\101\112",function()assert.is_nil(u:CurrentStep())assert.is_true(u:Progress(s))assert.is_table(u:CurrentStep())assert.equal(1,(u:CurrentStep()).id)assert.equal("\115\116\101\112\049",(u:CurrentStep()).name)assert.is_true(u:Progress(M))assert.is_table(u:CurrentStep())assert.equal(2,(u:CurrentStep()).id)assert.equal("\115\116\101\112\050",(u:CurrentStep()).name)assert.is_true(u:Progress(X))assert.is_table(u:CurrentStep())assert.equal(3,(u:CurrentStep()).id)assert.equal("\115\116\101\112\051",(u:CurrentStep()).name)assert.is_true(u:PreFinish())assert.is_nil(u:CurrentStep())assert.is_true(u:Finish())assert.is_nil(u:CurrentStep())end)end)describe("\035\078\101\120\116\083\116\101\112\115\073\100\115",function()it("\114\101\116\117\114\110\115 \116\104\101 \110\101\120\116 \115\116\101\112\115 \105\100\115",function()assert.same({1},u:NextStepsIds())assert.is_true(u:Progress(s))assert.same({2;3},u:NextStepsIds())assert.is_true(u:Progress(M))assert.same({3},u:NextStepsIds())assert.is_true(u:Progress(X))assert.same({},u:NextStepsIds())assert.is_true(u:PreFinish())assert.same({},u:NextStepsIds())assert.is_true(u:Finish())assert.same({},u:NextStepsIds())end)end)describe("\035\078\101\120\116\083\116\101\112\115",function()it("\114\101\116\117\114\110\115 \116\104\101 \110\101\120\116 \115\116\101\112\115",function()assert.same({1},j(u:NextSteps()))assert.is_true(u:Progress(s))assert.same({2;3},j(u:NextSteps()))assert.is_true(u:Progress(M))assert.same({3},j(u:NextSteps()))assert.is_true(u:Progress(X))assert.same({},u:NextSteps())assert.is_true(u:PreFinish())assert.same({},u:NextSteps())assert.is_true(u:Finish())assert.same({},u:NextSteps())end)end)describe("\035\080\114\111\103\114\101\115\115",function()describe("\119\105\116\104 \118\097\108\105\100 \097\098\105\108\105\116\105\101\115",function()it("\112\114\111\103\114\101\115\115\101\115 \116\104\101 \099\111\109\098\111",function()assert.is_false(u.started)assert.is_false(u.failed)assert.is_false(u.preFinished)assert.is_false(u.finished)assert.equal(0,u.count)assert.same({},u.waitQueue);(assert.spy(u.clock)).was_not_called()assert.is_nil(u:CurrentStep())assert.is_nil(u:CurrentStepId())assert.same({1},u:NextStepsIds())assert.same({1},j(u:NextSteps()))assert.is_true(u:Progress(s))assert.is_true(u.started)assert.is_false(u.failed)assert.is_false(u.preFinished)assert.is_false(u.finished)assert.equal(1,u.count)assert.same({10.5},u.waitQueue);(assert.spy(u.clock)).was_called(2)assert.same(1,(u:CurrentStep()).id)assert.equal(1,u:CurrentStepId())assert.same({2;3},u:NextStepsIds())assert.same({2;3},j(u:NextSteps()))assert.is_true(u:Progress(M))assert.is_true(u.started)assert.is_false(u.failed)assert.is_false(u.preFinished)assert.is_false(u.finished)assert.equal(2,u.count)assert.same({10.5;10.3},u.waitQueue);(assert.spy(u.clock)).was_called(4)assert.same(2,(u:CurrentStep()).id)assert.equal(2,u:CurrentStepId())assert.same({3},u:NextStepsIds())assert.same({3},j(u:NextSteps()))assert.is_true(u:Progress(X))assert.is_true(u.started)assert.is_false(u.failed)assert.is_false(u.preFinished)assert.is_false(u.finished)assert.equal(3,u.count)assert.same({10.5,10.3;10.25},u.waitQueue);(assert.spy(u.clock)).was_called(6)assert.same(3,(u:CurrentStep()).id)assert.equal(3,u:CurrentStepId())assert.same({},u:NextStepsIds())assert.same({},j(u:NextSteps()))end)it("\101\110\097\098\108\101\115 \115\107\105\112\112\105\110\103 \111\112\116\105\111\110\097\108 \115\116\101\112\115",function()assert.is_true(u:Progress(s))assert.is_true(u:Progress(X))end)end)describe("\119\105\116\104 \097\098\105\108\105\116\105\101\115 \108\105\115\116\101\100 \105\110 \116\104\101 \119\097\105\116 \115\112\101\099\105\097\108 \116\097\098\108\101",function()local p={id=G.uniqueId();sequence={{id=1,name=c.ABILITY_ALACRITY,required=true;next={2}};{id=2,name=c.ABILITY_COLD_SNAP;required=true,next={3}};{id=3,name=c.ABILITY_SUN_STRIKE;required=true}}}local q=(((G.chain(w)):omit({"\105\100";"\115\101\113\117\101\110\099\101"})):extend(p)):value()local Q=z("\097\098\105\108\105\116\121",{name=c.ABILITY_ALACRITY,special={duration=3.5}})local T=z("\097\098\105\108\105\116\121",{name=c.ABILITY_COLD_SNAP;special={duration=1.25}})local j=z("\097\098\105\108\105\116\121",{name=c.ABILITY_SUN_STRIKE,special={delay=2.6}})it("\117\115\101\115 \097\098\105\108\105\116\105\101\115 \115\112\101\099\105\097\108 \118\097\108\117\101\115 \102\111\114 \119\097\105\116 \116\105\109\101\115",function()u=f(q,{clock=g})assert.same({},u.waitQueue)assert.is_true(u:Progress(Q))assert.same({13.5},u.waitQueue)assert.is_true(u:Progress(T))assert.same({13.5;11.25},u.waitQueue)assert.is_true(u:Progress(j))assert.same({13.5,11.25;12.6},u.waitQueue)end)end)describe("\119\105\116\104 \105\110\118\097\108\105\100 \097\098\105\108\105\116\105\101\115",function()local G=z("\097\098\105\108\105\116\121",{name="\105\110\118\097\108\105\100",AbilityDuration=.5})it("\100\111\101\115 \078\079\084 \112\114\111\103\114\101\115\115 \116\104\101 \099\111\109\098\111",function()assert.is_false(u.started)assert.is_false(u.failed)assert.is_false(u.preFinished)assert.is_false(u.finished)assert.equal(0,u.count)assert.same({},u.waitQueue);(assert.spy(u.clock)).was_not_called()assert.is_nil(u:CurrentStep())assert.is_nil(u:CurrentStepId())assert.same({1},u:NextStepsIds())assert.same({1},j(u:NextSteps()))assert.is_false(u:Progress(G))assert.is_false(u.started)assert.is_false(u.failed)assert.is_false(u.preFinished)assert.is_false(u.finished)assert.equal(0,u.count)assert.same({},u.waitQueue);(assert.spy(u.clock)).was_not_called()assert.is_nil(u:CurrentStep())assert.is_nil(u:CurrentStepId())assert.same({1},u:NextStepsIds())assert.same({1},j(u:NextSteps()))assert.is_true(u:Progress(s))assert.is_true(u.started)assert.is_false(u.failed)assert.is_false(u.preFinished)assert.is_false(u.finished)assert.equal(1,u.count)assert.same({10.5},u.waitQueue);(assert.spy(u.clock)).was_called(2)assert.same(1,(u:CurrentStep()).id)assert.equal(1,u:CurrentStepId())assert.same({2;3},u:NextStepsIds())assert.same({2,3},j(u:NextSteps()))assert.is_false(u:Progress(G))assert.is_true(u.started)assert.is_false(u.failed)assert.is_false(u.preFinished)assert.is_false(u.finished)assert.equal(1,u.count)assert.same({10.5},u.waitQueue);(assert.spy(u.clock)).was_called(2)assert.same(1,(u:CurrentStep()).id)assert.equal(1,u:CurrentStepId())assert.same({2;3},u:NextStepsIds())assert.same({2;3},j(u:NextSteps()))end)end)end)describe("\035\070\097\105\108",function()it("\102\108\097\103\115 \116\104\101 \099\111\109\098\111 \097\115 \102\097\105\108\101\100",function()assert.is_false(u.failed)u:Fail()assert.is_true(u.failed)end)end)describe("\035\080\114\101\070\105\110\105\115\104",function()it("\097\100\118\097\110\099\101\115 \116\104\101 \099\111\109\098\111 \116\111 \112\114\101-\102\105\110\105\115\104\101\100 \115\116\097\116\101",function()assert.is_false(u:PreFinish())assert.is_false(u.preFinished)assert.is_true(u:Progress(s))assert.is_false(u:PreFinish())assert.is_false(u.preFinished)assert.is_true(u:Progress(M))assert.is_false(u:PreFinish())assert.is_false(u.preFinished)assert.is_true(u:Progress(X))assert.is_true(u:PreFinish())assert.is_true(u.preFinished)end)it("\115\101\116\115 \116\104\101 \119\097\105\116 \116\105\109\101\115",function()assert.is_true(u:Progress(s))assert.is_true(u:Progress(M))assert.is_true(u:Progress(X))assert.is_true(u:PreFinish())assert.equal(10.5,u.waitTime)assert.equal(.5,u.waitDuration)end)end)describe("\035\070\105\110\105\115\104",function()it("\097\100\118\097\110\099\101\115 \116\104\101 \099\111\109\098\111 \116\111 \102\105\110\105\115\104\101\100 \115\116\097\116\101",function()assert.is_false(u:Finish())assert.is_false(u.finished)assert.is_true(u:Progress(s))assert.is_false(u:Finish())assert.is_false(u.finished)assert.is_true(u:Progress(M))assert.is_false(u:Finish())assert.is_false(u.finished)assert.is_true(u:Progress(X))assert.is_false(u:Finish())assert.is_false(u.finished)assert.is_true(u:PreFinish())assert.is_true(u:Finish())assert.is_true(u.finished)end)it("\115\101\116\115 \116\104\101 \099\111\109\098\111 \100\117\114\097\116\105\111\110",function()local G=0 local function z()G=G+1 return G end u=f(w,{clock=z})assert.is_true(u:Progress(s))assert.is_true(u:Progress(M))assert.is_true(u:Progress(X))assert.is_true(u:PreFinish())assert.is_true(u:Finish())assert.equal(7,u.duration)end)end)describe("\035\073\110\099\114\101\109\101\110\116\068\097\109\097\103\101",function()it("\105\110\099\114\101\109\101\110\116\115 \116\104\101 \100\097\109\097\103\101 \097\099\099\117\109\117\108\097\116\111\114 \097\110\100 \114\101\116\117\114\110\115 \116\104\101 \097\099\099\117\109\117\108\097\116\101\100 \118\097\108\117\101",function()assert.equal(13,u:IncrementDamage(13))assert.equal(13,u.damage)assert.equal(44,u:IncrementDamage(31))assert.equal(44,u.damage)end)end)end)
local m = require("moses")
local create = require("support.factory").create

local Combo = require("invokation.combos.Combo")
local Talents = require("invokation.dota2.talents")
local BaseCombo = require("invokation.combos.BaseCombo")
local ComboSequence = require("invokation.combos.ComboSequence")

local INVOKER = require("invokation.const.invoker")

local SPEC = {
  id = 13,
  specialty = "qw",
  stance = "defensive",
  heroLevel = 25,
  damageRating = 5,
  difficultyRating = 5,
  gold = 1234,
  tags = { "late-game" },
  items = { "item_blink" },
  orbs = { 7, 7, 7 },
  talents = Talents.Select(Talents.L10_LEFT, Talents.L15_RIGHT, Talents.L20_RIGHT, Talents.L25_RIGHT),
  sequence = {
    { id = 1, name = "step1", required = true, next = { 2, 3 } },
    { id = 2, name = "step2", next = { 3 } },
    { id = 3, name = "step3", required = true },
  },
}

describe("Combo", function()
  local combo, spyClock

  local function clock()
    return 10
  end

  local getId = m.property("id")
  local function stepsIds(arr)
    return m.map(arr, getId)
  end

  local ability1 = create("ability", { name = "step1", AbilityDuration = 0.5 })
  local ability2 = create("ability", { name = "step2", AbilityDuration = 0.3 })
  local ability3 = create("ability", { name = "step3", AbilityDuration = 0.25 })

  before_each(function()
    spyClock = spy.new(clock)
    combo = Combo(SPEC, { clock = spyClock })
  end)

  it("inherits from BaseCombo", function()
    assert.is_true(BaseCombo:class_of(combo))
  end)

  describe("constructor", function()
    it("initializes attributes", function()
      for key, value in pairs(m.omit(SPEC, { "sequence" })) do
        assert.same(SPEC[key], value)
      end

      assert.is_false(combo.started)
      assert.is_false(combo.failed)
      assert.is_false(combo.finished)
      assert.equal(0, combo.count)
      assert.equal(0, combo.damage)
      assert.equal(spyClock, combo.clock)
      assert.is_true(ComboSequence:class_of(combo.sequence))
    end)
  end)

  describe("#todot", function()
    it("delegates to sequence", function()
      local spyTodot = spy.on(combo.sequence, "todot")
      combo:todot()
      assert.spy(spyTodot).was_called(1)
    end)
  end)

  describe("#CurrentStepId", function()
    it("returns the current step id", function()
      assert.is_nil(combo:CurrentStepId())
      assert.is_true(combo:Progress(ability1))
      assert.equal(1, combo:CurrentStepId())
      assert.is_true(combo:Progress(ability2))
      assert.equal(2, combo:CurrentStepId())
      assert.is_true(combo:Progress(ability3))
      assert.equal(3, combo:CurrentStepId())
      assert.is_true(combo:PreFinish())
      assert.is_nil(combo:CurrentStepId())
      assert.is_true(combo:Finish())
      assert.is_nil(combo:CurrentStepId())
    end)
  end)

  describe("#CurrentStep", function()
    it("returns the current step", function()
      assert.is_nil(combo:CurrentStep())

      assert.is_true(combo:Progress(ability1))
      assert.is_table(combo:CurrentStep())
      assert.equal(1, combo:CurrentStep().id)
      assert.equal("step1", combo:CurrentStep().name)

      assert.is_true(combo:Progress(ability2))
      assert.is_table(combo:CurrentStep())
      assert.equal(2, combo:CurrentStep().id)
      assert.equal("step2", combo:CurrentStep().name)

      assert.is_true(combo:Progress(ability3))
      assert.is_table(combo:CurrentStep())
      assert.equal(3, combo:CurrentStep().id)
      assert.equal("step3", combo:CurrentStep().name)

      assert.is_true(combo:PreFinish())
      assert.is_nil(combo:CurrentStep())
      assert.is_true(combo:Finish())
      assert.is_nil(combo:CurrentStep())
    end)
  end)

  describe("#NextStepsIds", function()
    it("returns the next steps ids", function()
      assert.same({ 1 }, combo:NextStepsIds())
      assert.is_true(combo:Progress(ability1))
      assert.same({ 2, 3 }, combo:NextStepsIds())
      assert.is_true(combo:Progress(ability2))
      assert.same({ 3 }, combo:NextStepsIds())
      assert.is_true(combo:Progress(ability3))
      assert.same({}, combo:NextStepsIds())
      assert.is_true(combo:PreFinish())
      assert.same({}, combo:NextStepsIds())
      assert.is_true(combo:Finish())
      assert.same({}, combo:NextStepsIds())
    end)
  end)

  describe("#NextSteps", function()
    it("returns the next steps", function()
      assert.same({ 1 }, stepsIds(combo:NextSteps()))
      assert.is_true(combo:Progress(ability1))
      assert.same({ 2, 3 }, stepsIds(combo:NextSteps()))
      assert.is_true(combo:Progress(ability2))
      assert.same({ 3 }, stepsIds(combo:NextSteps()))
      assert.is_true(combo:Progress(ability3))
      assert.same({}, combo:NextSteps())
      assert.is_true(combo:PreFinish())
      assert.same({}, combo:NextSteps())
      assert.is_true(combo:Finish())
      assert.same({}, combo:NextSteps())
    end)
  end)

  describe("#Progress", function()
    describe("with valid abilities", function()
      it("progresses the combo", function()
        assert.is_false(combo.started)
        assert.is_false(combo.failed)
        assert.is_false(combo.preFinished)
        assert.is_false(combo.finished)
        assert.equal(0, combo.count)
        assert.same({}, combo.waitQueue)
        assert.spy(combo.clock).was_not_called()
        assert.is_nil(combo:CurrentStep())
        assert.is_nil(combo:CurrentStepId())
        assert.same({ 1 }, combo:NextStepsIds())
        assert.same({ 1 }, stepsIds(combo:NextSteps()))

        assert.is_true(combo:Progress(ability1))

        assert.is_true(combo.started)
        assert.is_false(combo.failed)
        assert.is_false(combo.preFinished)
        assert.is_false(combo.finished)
        assert.equal(1, combo.count)
        assert.same({ 10.5 }, combo.waitQueue)
        assert.spy(combo.clock).was_called(2)
        assert.same(1, combo:CurrentStep().id)
        assert.equal(1, combo:CurrentStepId())
        assert.same({ 2, 3 }, combo:NextStepsIds())
        assert.same({ 2, 3 }, stepsIds(combo:NextSteps()))

        assert.is_true(combo:Progress(ability2))

        assert.is_true(combo.started)
        assert.is_false(combo.failed)
        assert.is_false(combo.preFinished)
        assert.is_false(combo.finished)
        assert.equal(2, combo.count)
        assert.same({ 10.5, 10.3 }, combo.waitQueue)
        assert.spy(combo.clock).was_called(4)
        assert.same(2, combo:CurrentStep().id)
        assert.equal(2, combo:CurrentStepId())
        assert.same({ 3 }, combo:NextStepsIds())
        assert.same({ 3 }, stepsIds(combo:NextSteps()))

        assert.is_true(combo:Progress(ability3))

        assert.is_true(combo.started)
        assert.is_false(combo.failed)
        assert.is_false(combo.preFinished)
        assert.is_false(combo.finished)
        assert.equal(3, combo.count)
        assert.same({ 10.5, 10.3, 10.25 }, combo.waitQueue)
        assert.spy(combo.clock).was_called(6)
        assert.same(3, combo:CurrentStep().id)
        assert.equal(3, combo:CurrentStepId())
        assert.same({}, combo:NextStepsIds())
        assert.same({}, stepsIds(combo:NextSteps()))
      end)

      it("enables skipping optional steps", function()
        assert.is_true(combo:Progress(ability1))
        assert.is_true(combo:Progress(ability3))
      end)
    end)

    describe("with abilities listed in the wait special table", function()
      local specFields = {
        id = m.uniqueId(),
        sequence = {
          { id = 1, name = INVOKER.ABILITY_ALACRITY, required = true, next = { 2 } },
          { id = 2, name = INVOKER.ABILITY_COLD_SNAP, required = true, next = { 3 } },
          { id = 3, name = INVOKER.ABILITY_SUN_STRIKE, required = true },
        },
      }

      local spec = m.chain(SPEC):omit({ "id", "sequence" }):extend(specFields):value()

      local abilityAlacrity = create("ability", { name = INVOKER.ABILITY_ALACRITY, special = { duration = 3.5 } })

      local abilityColdSnap = create("ability", {
        name = INVOKER.ABILITY_COLD_SNAP,
        special = { duration = 1.25 },
      })

      local abilitySunStrike = create("ability", { name = INVOKER.ABILITY_SUN_STRIKE, special = { delay = 2.6 } })

      it("uses abilities special values for wait times", function()
        combo = Combo(spec, { clock = spyClock })

        assert.same({}, combo.waitQueue)

        assert.is_true(combo:Progress(abilityAlacrity))
        assert.same({ 13.5 }, combo.waitQueue)

        assert.is_true(combo:Progress(abilityColdSnap))
        assert.same({ 13.5, 11.25 }, combo.waitQueue)

        assert.is_true(combo:Progress(abilitySunStrike))
        assert.same({ 13.5, 11.25, 12.6 }, combo.waitQueue)
      end)
    end)

    describe("with invalid abilities", function()
      local invalidAbility = create("ability", { name = "invalid", AbilityDuration = 0.5 })

      it("does NOT progress the combo", function()
        assert.is_false(combo.started)
        assert.is_false(combo.failed)
        assert.is_false(combo.preFinished)
        assert.is_false(combo.finished)
        assert.equal(0, combo.count)
        assert.same({}, combo.waitQueue)
        assert.spy(combo.clock).was_not_called()
        assert.is_nil(combo:CurrentStep())
        assert.is_nil(combo:CurrentStepId())
        assert.same({ 1 }, combo:NextStepsIds())
        assert.same({ 1 }, stepsIds(combo:NextSteps()))

        assert.is_false(combo:Progress(invalidAbility))

        assert.is_false(combo.started)
        assert.is_false(combo.failed)
        assert.is_false(combo.preFinished)
        assert.is_false(combo.finished)
        assert.equal(0, combo.count)
        assert.same({}, combo.waitQueue)
        assert.spy(combo.clock).was_not_called()
        assert.is_nil(combo:CurrentStep())
        assert.is_nil(combo:CurrentStepId())
        assert.same({ 1 }, combo:NextStepsIds())
        assert.same({ 1 }, stepsIds(combo:NextSteps()))

        assert.is_true(combo:Progress(ability1))

        assert.is_true(combo.started)
        assert.is_false(combo.failed)
        assert.is_false(combo.preFinished)
        assert.is_false(combo.finished)
        assert.equal(1, combo.count)
        assert.same({ 10.5 }, combo.waitQueue)
        assert.spy(combo.clock).was_called(2)
        assert.same(1, combo:CurrentStep().id)
        assert.equal(1, combo:CurrentStepId())
        assert.same({ 2, 3 }, combo:NextStepsIds())
        assert.same({ 2, 3 }, stepsIds(combo:NextSteps()))

        assert.is_false(combo:Progress(invalidAbility))

        assert.is_true(combo.started)
        assert.is_false(combo.failed)
        assert.is_false(combo.preFinished)
        assert.is_false(combo.finished)
        assert.equal(1, combo.count)
        assert.same({ 10.5 }, combo.waitQueue)
        assert.spy(combo.clock).was_called(2)
        assert.same(1, combo:CurrentStep().id)
        assert.equal(1, combo:CurrentStepId())
        assert.same({ 2, 3 }, combo:NextStepsIds())
        assert.same({ 2, 3 }, stepsIds(combo:NextSteps()))
      end)
    end)
  end)

  describe("#Fail", function()
    it("flags the combo as failed", function()
      assert.is_false(combo.failed)
      combo:Fail()
      assert.is_true(combo.failed)
    end)
  end)

  describe("#PreFinish", function()
    it("advances the combo to pre-finished state", function()
      assert.is_false(combo:PreFinish())
      assert.is_false(combo.preFinished)
      assert.is_true(combo:Progress(ability1))
      assert.is_false(combo:PreFinish())
      assert.is_false(combo.preFinished)
      assert.is_true(combo:Progress(ability2))
      assert.is_false(combo:PreFinish())
      assert.is_false(combo.preFinished)
      assert.is_true(combo:Progress(ability3))
      assert.is_true(combo:PreFinish())
      assert.is_true(combo.preFinished)
    end)

    it("sets the wait times", function()
      assert.is_true(combo:Progress(ability1))
      assert.is_true(combo:Progress(ability2))
      assert.is_true(combo:Progress(ability3))
      assert.is_true(combo:PreFinish())
      assert.equal(10.5, combo.waitTime)
      assert.equal(0.5, combo.waitDuration)
    end)
  end)

  describe("#Finish", function()
    it("advances the combo to finished state", function()
      assert.is_false(combo:Finish())
      assert.is_false(combo.finished)
      assert.is_true(combo:Progress(ability1))
      assert.is_false(combo:Finish())
      assert.is_false(combo.finished)
      assert.is_true(combo:Progress(ability2))
      assert.is_false(combo:Finish())
      assert.is_false(combo.finished)
      assert.is_true(combo:Progress(ability3))
      assert.is_false(combo:Finish())
      assert.is_false(combo.finished)
      assert.is_true(combo:PreFinish())
      assert.is_true(combo:Finish())
      assert.is_true(combo.finished)
    end)

    it("sets the combo duration", function()
      local clockIdx = 0
      local function incrClock()
        clockIdx = clockIdx + 1
        return clockIdx
      end

      combo = Combo(SPEC, { clock = incrClock })

      assert.is_true(combo:Progress(ability1))
      assert.is_true(combo:Progress(ability2))
      assert.is_true(combo:Progress(ability3))
      assert.is_true(combo:PreFinish())
      assert.is_true(combo:Finish())
      assert.equal(7, combo.duration)
    end)
  end)

  describe("#IncrementDamage", function()
    it("increments the damage accumulator and returns the accumulated value", function()
      assert.equal(13, combo:IncrementDamage(13))
      assert.equal(13, combo.damage)
      assert.equal(44, combo:IncrementDamage(31))
      assert.equal(44, combo.damage)
    end)
  end)
end)
