describe("Tests functionality of Geyser.Button", function()
  describe("Tests the functionality of Geyser.Button:new", function()
    it("creates a button with certain defaults if called with no constraints", function()
      local gb = Geyser.Button:new()
      assert.equals("table", type(gb))
      assert.equals("50px", gb.height)
      assert.equals(gb.height, gb.width)
      assert.is_truthy(gb.name:find("button"))
      assert.equals("look", gb.clickCommand)
      assert.equals("look", gb.downCommand)
      assert.equals("blue", gb.downColor)
      assert.equals("blue", gb.color)
      assert.equals("<center>Look</center>", gb.msg)
      assert.equals("<center>Look</center>", gb.downMsg)
      assert.equals("up", gb.state)
      assert.equals(5, gb.toolTipDuration)
      assert.equals("Click to look", gb.tooltip)
      assert.equals("Click to look", gb.downTooltip)
      assert.is_false(gb.twoState)
      gb:hide()
    end)
  end)

  describe("Tests the functionality of Geyser.Button:press", function()
    local eaSpy
    local pcSpy
    local gb
    before_each(function()
      eaSpy = spy.on(_G, "expandAlias")
      pcSpy = spy.on(_G, "pcall")
      gb = Geyser.Button:new({
        name = "testButton",
      })
    end)
    after_each(function()
      expandAlias:revert()
      pcall:revert()
    end)

    it('uses expandAlias to send clickCommand in "up" state', function()
      gb:press()
      assert.spy(eaSpy).was.called(1)
      assert.spy(eaSpy).was.called_with("look")
    end)

    it("does not change the state if twoState is false", function()
      gb:press()
      assert.is_false(gb.twoState)
      assert.equals("up", gb.state)
    end)

    it("does change the state if twoState is true", function()
      gb:enableTwoState()
      gb:press()
      assert.equals("down", gb.state)
      gb:press()
      assert.equals("up", gb.state)
    end)

    it('calls expandAlias with the downCommand if twoState is enabled and state is "down"', function()
      gb:enableTwoState()
      gb:setState("down")
      gb:press()
      assert.spy(eaSpy).was.called(1)
      assert.spy(eaSpy).was.called_with(gb.downCommand)
    end)

    it("uses pcall with the clickFunction if set", function()
      local cf = function() end
      gb:setClickFunction(cf)
      gb:press()
      assert.spy(pcSpy).was.called(1)
      assert.spy(pcSpy).was.called_with(cf)
    end)

    it('uses pcall with the downFunction if set and state is "down"', function()
      local df = function() end
      gb:setDownFunction(df)
      gb:enableTwoState()
      gb:setState("down")
      gb:press()
      assert.spy(pcSpy).was.called(1)
      assert.spy(pcSpy).was.called_with(df)
    end)
  end)

  describe("Tests the functionality of Geyser.Button:setState", function()
    local gb
    local echoSpy, styleSheetSpy, colorSpy, toolTipSpy
    before_each(function()
      echoSpy = spy.on(Geyser.Label, "echo")
      styleSheetSpy = spy.on(Geyser.Label, "setStyleSheet")
      colorSpy = spy.on(Geyser.Label, "setColor")
      toolTipSpy = spy.on(Geyser.Label, "setToolTip")
      gb = Geyser.Button:new({
        name = "testButton",
        twoState = true,
      })
    end)
    after_each(function()
      Geyser.Label.echo:revert()
      Geyser.Label.setStyleSheet:revert()
      Geyser.Label.setColor:revert()
      Geyser.Label.setToolTip:revert()
    end)

    it("should call echo with the message for the state set", function()
      gb:setState("up")
      assert.spy(echoSpy).was.called()
      assert.spy(echoSpy).was.called_with(match.is_ref(gb), gb.msg)
      gb:setState("down")
      assert.spy(echoSpy).was.called()
      assert.spy(echoSpy).was.called_with(match.is_ref(gb), gb.downMsg)
    end)

    it("should call setColor with the color for the state being set", function()
      gb:setState("up")
      assert.spy(colorSpy).was.called()
      assert.spy(colorSpy).was.called_with(match.is_ref(gb), gb.color)
      gb:setState("down")
      assert.spy(colorSpy).was.called()
      assert.spy(colorSpy).was.called_with(match.is_ref(gb), gb.downColor)
    end)

    it("should call setStyleSheet instead of setColor if the stylesheet is set", function()
      gb.downStyle = [[background-color: blue;]]
      gb.style = [[background-color: black;]]
      gb:setState("up")
      assert.spy(styleSheetSpy).was.called()
      assert.spy(styleSheetSpy).was.called_with(match.is_ref(gb), gb.style)
      gb:setState("down")
      assert.spy(styleSheetSpy).was.called()
      assert.spy(styleSheetSpy).was.called_with(match.is_ref(gb), gb.downStyle)
    end)

    it("should call setToolTip with the appropriate tool tip for the state being set", function()
      gb:setState("down")
      assert.spy(toolTipSpy).was.called()
      assert.spy(toolTipSpy).was.called_with(match.is_ref(gb), gb.tooltip, gb.toolTipDuration)
      gb:setState("up")
      assert.spy(toolTipSpy).was.called()
      assert.spy(toolTipSpy).was.called_with(match.is_ref(gb), gb.downTooltip, gb.toolTipDuration)
    end)
  end)

  -- The blocks above watch the calls a button makes; these assert what the
  -- widget ends up looking like, through getWindowGeometry and getLabelText.
  describe("Geyser.Button widget state", function()
    local created

    local function geometry(name)
      local x, y, width, height = getWindowGeometry(name)
      return { x = x, y = y, width = width, height = height }
    end

    local function track(object)
      created[#created + 1] = object
      return object
    end

    local function alive(object)
      if not object or not object.container or not object.container.windowList then
        return false
      end
      return object.container.windowList[object.name] == object
    end

    before_each(function()
      created = {}
    end)

    after_each(function()
      for _, object in ipairs(created) do
        if alive(object) then
          object:delete()
        end
      end
      created = {}
    end)

    it("creates a label widget at the constrained geometry", function()
      track(Geyser.Button:new({ name = "gbsGeometry", x = 5, y = 6, width = 70, height = 30 }))
      assert.are.equal("label", windowType("gbsGeometry"))
      assert.are.same({ x = 5, y = 6, width = 70, height = 30 }, geometry("gbsGeometry"))
      assert.is_true(windowVisible("gbsGeometry"))
    end)

    it("falls back to the button default size, not the label one", function()
      track(Geyser.Button:new({ name = "gbsDefaultSize", x = 0, y = 0 }))
      local actual = geometry("gbsDefaultSize")
      assert.are.equal(50, actual.width)
      assert.are.equal(50, actual.height)
    end)

    it("shows the up message on the label to start with", function()
      track(Geyser.Button:new({ name = "gbsUpMessage", x = 0, y = 0, width = 60, height = 20, msg = "press me" }))
      assert.is_truthy(getLabelText("gbsUpMessage"):find("press me", 1, true))
    end)

    it("swaps the label text with the state of a two state button", function()
      local button = track(Geyser.Button:new({
        name = "gbsTwoState",
        x = 0,
        y = 0,
        width = 60,
        height = 20,
        msg = "up text",
        downMsg = "down text",
        twoState = true,
      }))
      assert.is_truthy(getLabelText("gbsTwoState"):find("up text", 1, true))
      button:setState("down")
      assert.is_truthy(getLabelText("gbsTwoState"):find("down text", 1, true))
      button:setState("up")
      assert.is_truthy(getLabelText("gbsTwoState"):find("up text", 1, true))
    end)

    it("refuses to push a single state button down", function()
      local button = track(Geyser.Button:new({
        name = "gbsSingleState",
        x = 0,
        y = 0,
        width = 60,
        height = 20,
        msg = "up text",
        downMsg = "down text",
      }))
      local result, message = button:setState("down")
      assert.is_nil(result)
      assert.are.equal("cannot set a single state button's state to 'down', only 'up'", message)
      -- the refusal happens before anything is written, so neither the stored
      -- state nor the drawn message move
      assert.are.equal("up", button.state)
      assert.is_truthy(getLabelText("gbsSingleState"):find("up text", 1, true))
    end)

    it("keeps clicking a refused single state button on its up command", function()
      local clicks, downs = 0, 0
      local button = track(Geyser.Button:new({
        name = "gbsRefusedPress",
        x = 0,
        y = 0,
        width = 60,
        height = 20,
        clickFunction = function()
          clicks = clicks + 1
        end,
        downFunction = function()
          downs = downs + 1
        end,
      }))
      button:setState("down")
      button:press()
      assert.are.equal(1, clicks)
      assert.are.equal(0, downs)
      assert.are.equal("up", button.state)
    end)

    it("reports success for both states of a two state button", function()
      local button = track(Geyser.Button:new({
        name = "gbsStateResult",
        x = 0,
        y = 0,
        width = 60,
        height = 20,
        twoState = true,
      }))
      -- a legitimate 'down' has to be distinguishable from a refusal
      assert.is_true(button:setState("down"))
      assert.is_true(button:setState("up"))
    end)

    it("will not start a single state button in the down state", function()
      local button = track(Geyser.Button:new({
        name = "gbsDownConstraint",
        x = 0,
        y = 0,
        width = 60,
        height = 20,
        msg = "up text",
        state = "down",
      }))
      assert.are.equal("up", button.state)
      assert.is_truthy(getLabelText("gbsDownConstraint"):find("up text", 1, true))
    end)

    it("rejects a state that is not a string or not a known state", function()
      local button = track(Geyser.Button:new({ name = "gbsBadState", x = 0, y = 0, width = 60, height = 20 }))
      local result, message = button:setState(7)
      assert.is_nil(result)
      assert.is_truthy(message:find("state as string expected, got number", 1, true))
      local badResult, badMessage = button:setState("sideways")
      assert.is_nil(badResult)
      assert.is_truthy(badMessage:find("state must be one of 'up' or 'down'", 1, true))
      assert.are.equal("up", button.state)
    end)

    it("applies the stylesheet of the state it is put into", function()
      local button = track(Geyser.Button:new({
        name = "gbsStyles",
        x = 0,
        y = 0,
        width = 60,
        height = 20,
        twoState = true,
        style = "background-color: black;",
        downStyle = "background-color: blue;",
      }))
      button:setState("up")
      assert.are.equal("background-color: black;", getLabelStyleSheet("gbsStyles"))
      button:setState("down")
      assert.are.equal("background-color: blue;", getLabelStyleSheet("gbsStyles"))
    end)

    it("setMsg and setDownMsg redraw the button in its current state", function()
      local button = track(Geyser.Button:new({
        name = "gbsMessages",
        x = 0,
        y = 0,
        width = 60,
        height = 20,
        twoState = true,
      }))
      button:setMsg("new up")
      assert.is_truthy(getLabelText("gbsMessages"):find("new up", 1, true))
      button:setDownMsg("new down")
      button:setState("down")
      assert.is_truthy(getLabelText("gbsMessages"):find("new down", 1, true))
      local result, message = button:setMsg(42)
      assert.is_nil(result)
      assert.is_truthy(message:find("msg as string expected, got number", 1, true))
    end)

    it("press walks a two state button through both messages", function()
      local pressed = 0
      local button = track(Geyser.Button:new({
        name = "gbsPress",
        x = 0,
        y = 0,
        width = 60,
        height = 20,
        msg = "up text",
        downMsg = "down text",
        twoState = true,
        clickFunction = function()
          pressed = pressed + 1
        end,
        downFunction = function()
          pressed = pressed + 1
        end,
      }))
      button:press()
      assert.are.equal("down", button.state)
      assert.is_truthy(getLabelText("gbsPress"):find("down text", 1, true))
      button:press()
      assert.are.equal("up", button.state)
      assert.is_truthy(getLabelText("gbsPress"):find("up text", 1, true))
      assert.are.equal(2, pressed)
    end)

    it("disableTwoState puts the button back up", function()
      local button = track(Geyser.Button:new({
        name = "gbsDisableTwoState",
        x = 0,
        y = 0,
        width = 60,
        height = 20,
        msg = "up text",
        downMsg = "down text",
        twoState = true,
      }))
      button:setState("down")
      button:disableTwoState()
      assert.is_false(button.twoState)
      assert.are.equal("up", button.state)
      assert.is_truthy(getLabelText("gbsDisableTwoState"):find("up text", 1, true))
    end)

    it("hides and shows the button widget", function()
      local button = track(Geyser.Button:new({ name = "gbsVisible", x = 0, y = 0, width = 60, height = 20 }))
      button:hide()
      assert.is_false(windowVisible("gbsVisible"))
      button:show()
      assert.is_true(windowVisible("gbsVisible"))
    end)

    it("deletes its widget", function()
      local button = track(Geyser.Button:new({ name = "gbsDelete", x = 0, y = 0, width = 60, height = 20 }))
      button:delete()
      assert.is_nil(getWindowGeometry("gbsDelete"))
      assert.is_nil(Geyser.windowList.gbsDelete)
    end)

    describe("Geyser.Button command, function and style setters", function()
      local button

      before_each(function()
        button = track(
          Geyser.Button:new({ name = "gbsSetters", x = 0, y = 0, width = 60, height = 20, msg = "up", downMsg = "down" })
        )
        button:enableTwoState()
      end)

      it("setClickCommand and setDownCommand store the alias to expand per state", function()
        button:setClickCommand("say up")
        button:setDownCommand("say down")
        assert.are.equal("say up", button.clickCommand)
        assert.are.equal("say down", button.downCommand)

        local expand = spy.on(_G, "expandAlias")
        finally(function()
          expand:revert()
        end)
        button:press()
        assert.spy(expand).was.called_with("say up")
        button:press()
        assert.spy(expand).was.called_with("say down")
      end)

      it("setClickCommand and setDownCommand reject a non string", function()
        assert.has_error(function()
          button:setClickCommand(5)
        end)
        assert.has_error(function()
          button:setDownCommand(5)
        end)
      end)

      it("setDownColor and setColor apply the colour used for each state", function()
        -- a label's background colour has no getter, so watch what reaches
        -- setBackgroundColor; spy.on leaves the real call in place
        local backgroundColor = spy.on(_G, "setBackgroundColor")
        finally(function()
          backgroundColor:revert()
        end)

        local function lastColour()
          local calls = backgroundColor.calls
          local vals = calls[#calls].vals
          return { vals[1], vals[2], vals[3], vals[4] }
        end

        button:setColor("green")
        assert.are.equal("green", button.color)
        assert.are.same({ "gbsSetters", 0, 255, 0 }, lastColour())

        button:setDownColor("red")
        assert.are.equal("red", button.downColor)
        button:setState("down")
        assert.are.same({ "gbsSetters", 255, 0, 0 }, lastColour())

        button:setState("up")
        assert.are.same({ "gbsSetters", 0, 255, 0 }, lastColour())
      end)

      it("setStyle and setDownStyle put the right sheet on the widget per state", function()
        button:setStyle("background-color: green;")
        button:setDownStyle("background-color: red;")
        assert.are.equal("background-color: green;", button.style)
        assert.are.equal("background-color: red;", button.downStyle)

        button:setState("up")
        assert.are.equal("background-color: green;", getLabelStyleSheet("gbsSetters"))
        button:setState("down")
        assert.are.equal("background-color: red;", getLabelStyleSheet("gbsSetters"))
      end)

      it("setStyle accepts a Geyser.StyleSheet object", function()
        local sheet = Geyser.StyleSheet:new("background-color: blue;")
        button:setStyle(sheet)
        button:setState("up")
        assert.are.equal(sheet:getCSS(), getLabelStyleSheet("gbsSetters"))
      end)
    end)
  end)
end)
