# frozen_string_literal: true

require 'spec_helper'

describe "Stealth::Controller" do

  class MrRobotsController < Stealth::Controller
    def my_action
      [:success, :my_action]
    end

    def my_action2
      [:success, :my_action2]
    end

    def my_action3
      [:success, :my_action3]
    end
  end

  class MrTronsController < Stealth::Controller
    def other_action

    end

    def other_action2
      step_to state: :other_action4
    end

    def other_action3

    end

    def other_action4
      do_nothing
    end

    def broken_action
      raise StandardError
    end

    def parts_unknown
      step_to flow: :parts, state: :unknown
    end

    def halted
      halt!
      step_to state: :other_action2
    end
  end

  class FlowMap
    include Stealth::Flow

    flow :mr_robot do
      state :my_action
      state :my_action2
      state :my_action3
    end

    flow :mr_tron do
      state :other_action
      state :other_action2
      state :other_action3
      state :other_action4
      state :broken_action
      state :part_unknown
      state :halted
      state :deprecated_action, redirects_to: :other_action
      state :deprecated_action2, redirects_to: 'mr_robot->my_action'
    end
  end

  let(:facebook_message) { SampleMessage.new(service: 'facebook') }
  let(:controller) {
    MrTronsController.new(service_message: facebook_message.message_with_text)
  }

  describe "convenience methods" do
    it "should make the session ID accessible via current_session_id" do
      controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action')

      expect(controller.current_session_id).to eq(facebook_message.sender_id)
    end

    it "should make the message available in current_message.message" do
      expect(controller.current_message.message).to eq(facebook_message.message)
    end

    it "should make the payload available in current_message.payload" do
      message_with_payload = facebook_message.message_with_payload
      expect(controller.current_message.payload).to eq(message_with_payload.payload)
    end

    describe "current_service" do
      let(:twilio_message) { SampleMessage.new(service: 'twilio') }
      let(:controller_with_twilio_message) { MrTronsController.new(service_message: twilio_message.message_with_text) }

      it "should detect a Facebook message" do
        expect(controller.current_service).to eq('facebook')
      end

      it "should detect a Twilio message" do
        expect(controller_with_twilio_message.current_service).to eq('twilio')
      end
    end

    describe "messages with location" do
      let(:message_with_location) { facebook_message.message_with_location }
      let(:controller_with_location) { MrTronsController.new(service_message: message_with_location) }

      it "should make the location available in current_message.location" do
        expect(controller_with_location.current_message.location).to eq(message_with_location.location)
      end

      it "should return true for current_message.has_location?" do
        expect(controller_with_location.has_location?).to be true
      end
    end

    describe "messages with attachments" do
      let(:message_with_attachments) { facebook_message.message_with_attachments }
      let(:controller_with_attachment) { MrTronsController.new(service_message: message_with_attachments) }

      it "should make the attachments available in current_message.attachments" do
        expect(controller_with_attachment.current_message.attachments).to eq(message_with_attachments.attachments)
      end

      it "should return true for current_message.has_attachments?" do
        expect(controller_with_attachment.has_attachments?).to be true
      end
    end
  end

  describe "states with redirect_to specified" do
    it "should step_to the specified redirect state when only a state is specified" do
      controller.current_session.session = Stealth::Session.canonical_session_slug(flow: 'mr_tron', state: 'deprecated_action')
      expect(MrTronsController).to receive(:new).and_return(controller)
      expect(controller).to receive(:other_action)
      controller.action(action: :deprecated_action)
    end

    it "should step_to the specified redirect flow and state when a session is specified" do
      controller.current_session.session = Stealth::Session.canonical_session_slug(flow: 'mr_tron', state: 'deprecated_action2')
      mr_robot_controller = MrRobotsController.new(service_message: facebook_message.message_with_text)

      allow(MrRobotsController).to receive(:new).and_return(mr_robot_controller)
      expect(mr_robot_controller).to receive(:my_action)
      controller.action(action: :deprecated_action2)
    end

    it "should NOT call the redirected controller action method" do
      controller.current_session.session = Stealth::Session.canonical_session_slug(flow: 'mr_tron', state: 'deprecated_action')
      expect(MrTronsController).to receive(:new).and_return(controller)
      expect(controller).to_not receive(:deprecated_action)
      controller.action(action: :deprecated_action)
    end
  end

  describe "step_to" do
    it "should raise an ArgumentError if a session, flow, or state is not specified" do
      expect {
        controller.step_to
      }.to raise_error(ArgumentError)
    end

    it "should call the flow's first state's controller action when only a flow is provided" do
      expect_any_instance_of(MrRobotsController).to receive(:my_action)
      controller.step_to flow: "mr_robot"
    end

    it "should call a controller's corresponding action when only a state is provided" do
      expect_any_instance_of(MrTronsController).to receive(:other_action3)

      controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action')

      controller.step_to state: "other_action3"
    end

    it "should call a controller's corresponding action when a state and flow is provided" do
      expect_any_instance_of(MrRobotsController).to receive(:my_action3)
      controller.step_to flow: "mr_robot", state: "my_action3"
    end

    it "should call a controller's corresponding action when a session is provided" do
      expect_any_instance_of(MrRobotsController).to receive(:my_action3)

      allow(controller.current_session).to receive(:flow_string).and_return("mr_robot")
      allow(controller.current_session).to receive(:state_string).and_return("my_action3")

      controller.step_to session: controller.current_session
    end

    it "should call a controller's corresponding action when a session slug is provided" do
      expect_any_instance_of(MrRobotsController).to receive(:my_action3)
      controller.step_to slug: 'mr_robot->my_action3'
    end

    it "should pass along the service_message" do
      robot_controller_dbl = double('MrRobotsController').as_null_object
      expect(MrRobotsController).to receive(:new).with(service_message: controller.current_message, pos: nil).and_return(robot_controller_dbl)
      controller.step_to flow: :mr_robot, state: :my_action3
    end

    it "should accept flow and string specified as symbols" do
      expect_any_instance_of(MrRobotsController).to receive(:my_action3)
      controller.step_to flow: :mr_robot, state: :my_action3
    end

    it "should check if an interruption occured" do
      expect(controller).to receive(:interrupt_detected?).and_return(false)
      controller.step_to flow: :mr_robot, state: :my_action3
    end

    it "should call run_interrupt_action if an interruption occured and return" do
      expect(controller).to receive(:interrupt_detected?).and_return(true)
      expect(controller).to receive(:run_interrupt_action)
      expect(controller.step_to(flow: :mr_robot, state: :my_action3)).to eq :interrupted
    end

    it "should set @pos if it is specified in the arguments" do
      expect_any_instance_of(MrRobotsController).to receive(:my_action3)
      controller.step_to flow: :mr_robot, state: :my_action3, pos: -1
      expect(controller.pos).to eq -1
    end

    it "should leave @pos as nil if the pos argument is not specified" do
      expect_any_instance_of(MrRobotsController).to receive(:my_action3)
      controller.step_to flow: :mr_robot, state: :my_action3
      expect(controller.pos).to be_nil
    end
  end

  describe "update_session_to" do
    it "should raise an ArgumentError if a session, flow, or state is not specified" do
      expect {
        controller.update_session_to
      }.to raise_error(ArgumentError)
    end

    it "should update session to flow's first state's controller action when only a flow is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      controller.update_session_to flow: "mr_robot"
      expect(controller.current_session.flow_string).to eq('mr_robot')
      expect(controller.current_session.state_string).to eq('my_action')
    end

    it "should update session to controller's corresponding action when only a state is provided" do
      expect_any_instance_of(MrTronsController).to_not receive(:other_action3)

      controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action')

      controller.update_session_to state: "other_action3"
      expect(controller.current_session.flow_string).to eq('mr_tron')
      expect(controller.current_session.state_string).to eq('other_action3')
    end

    it "should update session to controller's corresponding action when a state and flow is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action3)

      controller.update_session_to flow: "mr_robot", state: "my_action3"
      expect(controller.current_session.flow_string).to eq('mr_robot')
      expect(controller.current_session.state_string).to eq('my_action3')
    end

    it "should update session to controller's corresponding action when a session is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action3)

      session = Stealth::Session.new(id: controller.current_session_id)
      session.set_session(new_flow: 'mr_robot', new_state: 'my_action3')

      controller.update_session_to session: session
      expect(controller.current_session.flow_string).to eq('mr_robot')
      expect(controller.current_session.state_string).to eq('my_action3')
    end

    it "should update session to controller's corresponding action when a session slug is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action3)
      expect(controller.current_session.flow_string).to eq('mr_robot')
      expect(controller.current_session.state_string).to eq('my_action3')

      controller.update_session_to slug: 'mr_robot->my_action3'
    end

    it "should accept flow and string specified as symbols" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action3)

      controller.update_session_to flow: :mr_robot, state: :my_action3
      expect(controller.current_session.flow_string).to eq('mr_robot')
      expect(controller.current_session.state_string).to eq('my_action3')
    end

    it "should check if an interruption occured" do
      expect(controller).to receive(:interrupt_detected?).and_return(false)
      controller.update_session_to flow: :mr_robot, state: :my_action3
    end

    it "should call run_interrupt_action if an interruption occured and return" do
      expect(controller).to receive(:interrupt_detected?).and_return(true)
      expect(controller).to receive(:run_interrupt_action)
      expect(controller.update_session_to(flow: :mr_robot, state: :my_action3)).to eq :interrupted
    end
  end

  describe "step_to_in" do
    it "should raise an ArgumentError if a session, flow, or state is not specified" do
      expect {
        controller.step_to_in
      }.to raise_error(ArgumentError)
    end

    it "should raise an ArgumentError if delay is not specifed as an ActiveSupport::Duration" do
      expect {
        controller.step_to_in DateTime.now, flow: 'mr_robot'
      }.to raise_error(ArgumentError)
    end

    it "should schedule a transition to flow's first state's controller action when only a flow is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with(
        100.seconds,
        controller.current_service,
        controller.current_session_id,
        'mr_robot',
        'my_action',
        nil
      )

      expect {
        controller.step_to_in 100.seconds, flow: "mr_robot"
      }.to_not change(controller.current_session, :get_session)
    end

    it "should schedule a transition to controller's corresponding action when only a state is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action')

      expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with(
        100.seconds,
        controller.current_service,
        controller.current_session_id,
        'mr_tron',
        'other_action3',
        nil
      )

      expect {
        controller.step_to_in 100.seconds, state: "other_action3"
      }.to_not change(controller.current_session, :get_session)
    end

    it "should update session to controller's corresponding action when a state and flow is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with(
        100.seconds,
        controller.current_service,
        controller.current_session_id,
        'mr_robot',
        'my_action3',
        nil
      )

      expect {
        controller.step_to_in 100.seconds, flow: 'mr_robot', state: "my_action3"
      }.to_not change(controller.current_session, :get_session)
    end

    it "should update session to controller's corresponding action when a session is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      session = Stealth::Session.new(id: controller.current_session_id)
      session.set_session(new_flow: 'mr_robot', new_state: 'my_action3')

      expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with(
        100.seconds,
        controller.current_service,
        controller.current_session_id,
        'mr_robot',
        'my_action3',
        nil
      )

      expect {
        controller.step_to_in 100.seconds, session: session
      }.to_not change(controller.current_session, :get_session)
    end

    it "should update session to controller's corresponding action when a session slug is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with(
        100.seconds,
        controller.current_service,
        controller.current_session_id,
        'mr_robot',
        'my_action3',
        nil
      )

      expect {
        controller.step_to_in 100.seconds, slug: 'mr_robot->my_action3'
      }.to_not change(controller.current_session, :get_session)
    end

    it "should accept flow and string specified as symbols" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with(
        100.seconds,
        controller.current_service,
        controller.current_session_id,
        'mr_robot',
        'my_action3',
        nil
      )

      expect {
        controller.step_to_in 100.seconds, flow: :mr_robot, state: :my_action3
      }.to_not change(controller.current_session, :get_session)
    end

    it "should pass along the target_id if set on the message" do
      expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with(
        100.seconds,
        controller.current_service,
        controller.current_session_id,
        'mr_robot',
        'my_action3',
        '+18885551212'
      )

      controller.current_message.target_id = '+18885551212'
      controller.step_to_in 100.seconds, flow: :mr_robot, state: :my_action3
    end

    it "should check if an interruption occured" do
      expect(controller).to receive(:interrupt_detected?).and_return(false)
      controller.step_to_in 100.seconds, flow: :mr_robot, state: :my_action3
    end

    it "should call run_interrupt_action if an interruption occured and return" do
      expect(controller).to receive(:interrupt_detected?).and_return(true)
      expect(controller).to receive(:run_interrupt_action)
      expect(controller.step_to_in(100.seconds, flow: :mr_robot, state: :my_action3)).to eq :interrupted
    end
  end

  describe "step_to_at" do
    let(:future_timestamp) { DateTime.now + 10.hours }

    it "should raise an ArgumentError if a session, flow, or state is not specified" do
      expect {
        controller.step_to_at
      }.to raise_error(ArgumentError)
    end

    it "should raise an ArgumentError if delay is not specifed as a DateTime" do
      expect {
        controller.step_to_at 100.seconds, flow: 'mr_robot'
      }.to raise_error(ArgumentError)
    end

    it "should schedule a transition to flow's first state's controller action when only a flow is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with(
        future_timestamp,
        controller.current_service,
        controller.current_session_id,
        'mr_robot',
        'my_action',
        nil
      )

      expect {
        controller.step_to_at future_timestamp, flow: "mr_robot"
      }.to_not change(controller.current_session, :get_session)
    end

    it "should schedule a transition to controller's corresponding action when only a state is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action')

      expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with(
        future_timestamp,
        controller.current_service,
        controller.current_session_id,
        'mr_tron',
        'other_action3',
        nil
      )

      expect {
        controller.step_to_at future_timestamp, state: "other_action3"
      }.to_not change(controller.current_session, :get_session)
    end

    it "should update session to controller's corresponding action when a state and flow is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with(
        future_timestamp,
        controller.current_service,
        controller.current_session_id,
        'mr_robot',
        'my_action3',
        nil
      )

      expect {
        controller.step_to_at future_timestamp, flow: 'mr_robot', state: "my_action3"
      }.to_not change(controller.current_session, :get_session)
    end

    it "should update session to controller's corresponding action when a session is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      session = Stealth::Session.new(id: controller.current_session_id)
      session.set_session(new_flow: 'mr_robot', new_state: 'my_action3')

      expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with(
        future_timestamp,
        controller.current_service,
        controller.current_session_id,
        'mr_robot',
        'my_action3',
        nil
      )

      expect {
        controller.step_to_at future_timestamp, session: session
      }.to_not change(controller.current_session, :get_session)
    end

    it "should update session to controller's corresponding action when a session slug is provided" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with(
        future_timestamp,
        controller.current_service,
        controller.current_session_id,
        'mr_robot',
        'my_action3',
        nil
      )

      expect {
        controller.step_to_at future_timestamp, slug: 'mr_robot->my_action3'
      }.to_not change(controller.current_session, :get_session)
    end

    it "should accept flow and string specified as symbols" do
      expect_any_instance_of(MrRobotsController).to_not receive(:my_action)

      expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with(
        future_timestamp,
        controller.current_service,
        controller.current_session_id,
        'mr_robot',
        'my_action3',
        nil
      )

      expect {
        controller.step_to_at future_timestamp, flow: :mr_robot, state: :my_action3
      }.to_not change(controller.current_session, :get_session)
    end

    it "should pass along the target_id if set on the message" do
      expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with(
        future_timestamp,
        controller.current_service,
        controller.current_session_id,
        'mr_robot',
        'my_action3',
        '+18885551212'
      )

      controller.current_message.target_id = '+18885551212'
      controller.step_to_at future_timestamp, flow: :mr_robot, state: :my_action3
    end

    it "should check if an interruption occured" do
      expect(controller).to receive(:interrupt_detected?).and_return(false)
      controller.step_to_at future_timestamp, flow: :mr_robot, state: :my_action3
    end

    it "should call run_interrupt_action if an interruption occured and return" do
      expect(controller).to receive(:interrupt_detected?).and_return(true)
      expect(controller).to receive(:run_interrupt_action)
      expect(controller.step_to_at(future_timestamp, flow: :mr_robot, state: :my_action3)).to eq :interrupted
    end
  end

  describe "set_back_to" do
    it "should raise an ArgumentError if a session, flow, or state is not specified" do
      expect {
        controller.set_back_to
      }.to raise_error(ArgumentError)
    end

    it "should call the flow's first state's controller action when only a flow is provided" do
      expect {
        controller.set_back_to(flow: :mr_robot)
      }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.to('mr_robot->my_action')
    end

    it "should call a controller's corresponding action when only a state is provided" do
      controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action')

      expect {
        controller.set_back_to(state: :other_action3)
      }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.to('mr_tron->other_action3')
    end

    it "should call a controller's corresponding action when a state and flow is provided" do
      expect {
        controller.set_back_to(flow: 'marco', state: 'polo')
      }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.to('marco->polo')
    end

    it "should call a controller's corresponding action when a session is provided" do
      allow(controller.current_session).to receive(:flow_string).and_return("mr_robot")
      allow(controller.current_session).to receive(:state_string).and_return("my_action3")

      expect {
        controller.set_back_to(session: controller.current_session)
      }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.to('mr_robot->my_action3')
    end

    it "should call a controller's corresponding action when a session slug is provided" do
      expect {
        controller.set_back_to(slug: 'marco->polo')
      }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.to('marco->polo')
    end

    it "should default to the scoped flow if one is not specified" do
      controller.current_session.set_session(new_flow: :mr_tron, new_state: :other_action)
      expect {
        controller.set_back_to(state: 'polo')
      }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.to('mr_tron->polo')
    end

    it "should overwrite the existing back_to_session if one is already present" do
      $redis.set([controller.current_session_id, 'back_to'].join('-'), 'marco->polo')
      controller.current_session.set_session(new_flow: :mr_tron, new_state: :other_action)
      expect {
        controller.set_back_to(state: 'other_action')
      }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.from('marco->polo').to('mr_tron->other_action')
    end

    it "should check if an interruption occured" do
      expect(controller).to receive(:interrupt_detected?).and_return(false)
      controller.set_back_to flow: :mr_robot, state: :my_action3
    end

    it "should call run_interrupt_action if an interruption occured and return" do
      expect(controller).to receive(:interrupt_detected?).and_return(true)
      expect(controller).to receive(:run_interrupt_action)
      expect(controller.set_back_to(flow: :mr_robot, state: :my_action3)).to eq :interrupted
    end
  end

  describe "step_back" do
    let(:back_to_slug) { [controller.current_session_id, 'back_to'].join('-') }

    it "should raise Stealth::Errors::InvalidStateTransition if back_to_session is not set" do
      $redis.del(back_to_slug)
      expect {
        controller.step_back
      }.to raise_error(Stealth::Errors::InvalidStateTransition)
    end

    it "should step_to the stored back_to_session" do
      controller.set_back_to(flow: 'marco', state: 'polo')
      back_to_session = Stealth::Session.new(
        id: controller.current_session_id,
        type: :back_to
      )

      # We need to control the returned session object so the IDs match
      expect(Stealth::Session).to receive(:new).with(
        id: controller.current_session_id,
        type: :back_to
      ).and_return(back_to_session)
      expect(controller).to receive(:step_to).with(session: back_to_session)

      controller.step_back
    end

    it "should check if an interruption occured" do
      controller.set_back_to(flow: :mr_robot, state: :my_action3)
      expect(controller).to receive(:interrupt_detected?).and_return(false)
      controller.step_back
    end

    it "should call run_interrupt_action if an interruption occured and return" do
      controller.set_back_to(flow: :mr_robot, state: :my_action3)
      expect(controller).to receive(:interrupt_detected?).and_return(true)
      expect(controller).to receive(:run_interrupt_action)
      expect(controller.step_back).to eq :interrupted
    end
  end

  describe "progressed?" do
    it "should be truthy if an action calls step_to" do
      expect(controller.progressed?).to be_falsey
      controller.step_to flow: "mr_robot"
      expect(controller.progressed?).to be_truthy
    end

    it "should be falsey if an action only calls step_to_at" do
      expect(controller.progressed?).to be_falsey

      expect(Stealth::ScheduledReplyJob).to receive(:perform_at)
      controller.step_to_at (DateTime.now + 10.hours), flow: 'mr_robot'

      expect(controller.progressed?).to be_falsey
    end

    it "should be falsey if an action only calls step_to_in" do
      expect(controller.progressed?).to be_falsey

      expect(Stealth::ScheduledReplyJob).to receive(:perform_in)
      controller.step_to_in 100.seconds, flow: 'mr_robot'

      expect(controller.progressed?).to be_falsey
    end

    it "should be truthy if an action calls update_session_to" do
      expect(controller.progressed?).to be_falsey
      controller.update_session_to flow: "mr_robot"
      expect(controller.progressed?).to be_truthy
    end

    it "should be truthy if an action sends replies" do
      expect(controller.progressed?).to be_falsey

      # Stub out a service reply -- we just want send_replies to succeed here
      stubbed_service_reply = double("service_reply")
      allow(controller).to receive(:action_replies).and_return([], :erb)
      allow(stubbed_service_reply).to receive(:replies).and_return([])
      allow(Stealth::ServiceReply).to receive(:new).and_return(stubbed_service_reply)

      controller.send_replies
      expect(controller.progressed?).to be_truthy
    end

    it "should be falsey otherwise" do
      allow(controller).to receive(:flow_controller).and_return(controller)
      expect(controller.progressed?).to be_falsey
      controller.action(action: :other_action)
      expect(controller.progressed?).to be_falsey
    end
  end

  describe "do_nothing" do
    it "should set progressed to truthy when called" do
      allow(controller).to receive(:flow_controller).and_return(controller)
      expect(controller.progressed?).to be_falsey
      controller.action(action: :other_action4)
      expect(controller.progressed?).to be_truthy
    end
  end

  describe "update_session" do
    before(:each) do
      controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action')
    end

    it "should set progressed to :updated_session" do
      controller.send(:update_session, flow: :mr_tron, state: :other_action)
      expect(controller.progressed?).to eq :updated_session
    end

    it "call set_session on the current_session with the new flow and state" do
      controller.send(:update_session, flow: :mr_robot, state: :my_action)
      expect(controller.current_session.flow_string).to eq 'mr_robot'
      expect(controller.current_session.state_string).to eq 'my_action'
    end

    it "should not call set_session on current_session if the flow and state match" do
      expect_any_instance_of(Stealth::Session).to_not receive(:set_session)
      controller.send(:update_session, flow: :mr_tron, state: :other_action)
    end
  end

  describe "dev jumps" do
    let!(:dev_env) { ActiveSupport::StringInquirer.new('development') }

    describe "dev_jump_detected?" do
      it "should return false if the enviornment is not 'development'" do
        expect(Stealth.env).to eq 'test'
        expect(controller.send(:dev_jump_detected?)).to be false
      end

      it "should return false if the message does not match the jump format" do
        allow(Stealth).to receive(:env).and_return(dev_env)
        controller.current_message.message = 'hello world'
        expect(Stealth.env.development?).to be true
        expect(controller.send(:dev_jump_detected?)).to be false
      end

      it "should return false if the message looks like an American date" do
        allow(Stealth).to receive(:env).and_return(dev_env)
        controller.current_message.message = '1/23/84'
        expect(Stealth.env.development?).to be true
        expect(controller.send(:dev_jump_detected?)).to be false
      end

      it "should return false if the message looks like an American date that is zero padded" do
        allow(Stealth).to receive(:env).and_return(dev_env)
        controller.current_message.message = '01/23/1984'
        expect(Stealth.env.development?).to be true
        expect(controller.send(:dev_jump_detected?)).to be false
      end

      describe "with a dev jump message" do
        before(:each) do
          expect(controller).to receive(:handle_dev_jump).and_return(true)
          expect(Stealth).to receive(:env).and_return(dev_env)
        end

        it "should return true if the message is in the format /flow/state" do
          controller.current_message.message = '/mr_robot/my_action'
          expect(controller.send(:dev_jump_detected?)).to be true
        end

        it "should return true if the message is in the format /flow" do
          controller.current_message.message = '/mr_robot'
          expect(controller.send(:dev_jump_detected?)).to be true
        end

        it "should return true if the message is in the format //state" do
          controller.current_message.message = '//my_action'
          expect(controller.send(:dev_jump_detected?)).to be true
        end
      end
    end

    describe "handle_dev_jump" do
      it "should handle messages in the format /flow/state" do
        controller.current_message.message = '/mr_robot/my_action'
        expect(controller).to receive(:step_to).with(flow: 'mr_robot', state: 'my_action')
        controller.send(:handle_dev_jump)
      end

      it "should handle messages in the format /flow" do
        controller.current_message.message = '/mr_robot'
        expect(controller).to receive(:step_to).with(flow: 'mr_robot', state: nil)
        controller.send(:handle_dev_jump)
      end

      it "should handle messages in the format //state" do
        controller.current_message.message = '//my_action'
        expect(controller).to receive(:step_to).with(flow: nil, state: 'my_action')
        controller.send(:handle_dev_jump)
      end
    end

    describe "session locking" do
      before(:each) do
        allow(MrTronsController).to receive(:new).and_return(controller)
      end

      it "should lock and then unlock a session when a do_nothing action is called" do
        expect(controller).to receive(:lock_session!).once
        expect(controller).to receive(:release_lock!).once
        controller.action(action: :other_action4)
      end

      it "should lock and then unlock a session twice when an action steps to another" do
        expect(controller).to receive(:lock_session!).twice
        expect(controller).to receive(:release_lock!).twice
        controller.action(action: :other_action2)
      end

      it 'should still release the lock even if an action raises' do
        expect(controller).to receive(:release_lock!).once
        controller.action(action: :broken_action)
      end

      it 'should still release the lock if an action steps to an unknown flow->state' do
        expect(controller).to receive(:release_lock!).once
        controller.action(action: :parts_unknown)
      end
    end

    describe "halt!" do
      it "should catch the error and log the sessio halt" do
        ### It's lame we have to include these two
        expect(Stealth::Logger).to receive(:l).with(
          topic: "primary_session",
          message: "User #{facebook_message.sender_id}: setting session to mr_tron->halted"
        )
        expect(Stealth::Logger).to receive(:l).with(
          topic: "previous_session",
          message: "User #{facebook_message.sender_id}: setting to parts->unknown"
        )
        ###

        expect(Stealth::Logger).to receive(:l).with(
          topic: "session",
          message: "User #{facebook_message.sender_id}: session halted."
        )

        controller.step_to(flow: :mr_tron, state: :halted)
      end

      it "should NOT continue with the rest of the controller code" do
        expect_any_instance_of(MrTronsController).to_not receive(:other_action2)
        expect_any_instance_of(MrTronsController).to_not receive(:step_to).with(state: :other_action2)
        controller.step_to(flow: :mr_tron, state: :halted)
      end
    end
  end

end
