require 'spec_helper'

class WechatController < ActionController::Base
  wechat_responder
end

RSpec.describe WechatController, type: :controller do
  def xml_to_hash(response)
    Hash.from_xml(response.body)['xml'].symbolize_keys
  end

  render_views

  let(:signature_params) do
    timestamp = '1234567'
    nonce = 'nonce'
    signature = Digest::SHA1.hexdigest([ENV['WECHAT_TOKEN'], timestamp, nonce].sort.join)
    { timestamp: timestamp, nonce: nonce, signature: signature }
  end

  let(:message_base) do
    {
      ToUserName: 'toUser',
      FromUserName: 'fromUser',
      CreateTime: '1348831860',
      MsgId: '1234567890123456'
    }
  end

  let(:text_message) { message_base.merge(MsgType: 'text', Content: 'text message') }

  specify 'config responder using global config' do
    expect(controller.class.wechat).to eq(Wechat.api)
    expect(controller.class.token).to eq(Wechat.config.token)
  end

  describe 'config responder using per controller configuration' do
    controller do
      wechat_responder appid: 'controller_appid', secret: 'controller_secret', token: 'controller_token',
                       access_token: 'controller_access_token',
                       agentid: 1, encoding_aes_key: 'encoding_aes_key'
    end
    specify 'will set controller wechat api and token' do
      access_token = controller.class.wechat.access_token
      expect(access_token.appid).to eq('controller_appid')
      expect(access_token.secret).to eq('controller_secret')
      expect(access_token.token_file).to eq('controller_access_token')
      expect(controller.class.token).to eq('controller_token')
      expect(controller.class.agentid).to eq(1)
      expect(controller.class.encrypt_mode).to eq(false)
      expect(controller.class.encoding_aes_key).to eq('encoding_aes_key')
    end
  end

  describe 'Verify signature' do
    specify 'on show action' do
      get :show, signature_params.merge(signature: 'invalid_signature')
      expect(response.code).to eq('403')
    end

    specify 'on create action' do
      post :create, signature_params.merge(signature: 'invalid_signature')
      expect(response.code).to eq('403')
    end
  end

  specify "echo 'echostr' param when show" do
    get :show, signature_params.merge(echostr: 'hello')
    expect(response.body).to eq('hello')
  end

  describe 'responders' do
    specify 'responders only accept :text, :image, :voice, :video, :location, :link, :event, :fallback message type' do
      [:text, :image, :voice, :video, :location, :link, :event, :fallback].each do |message_type|
        controller.class.on message_type, respond: 'response'
      end
    end

    specify 'will raise error if message type is unkonwn' do
      expect { controller.class.on :unkonwn, respond: 'response' }.to raise_error
    end

    specify 'responder take :with argument only for :text and :event message_type' do
      expect(controller.class.on :text, with: 'command', respond: 'response').to eq(with: 'command', respond: 'response')
      expect(controller.class.on :event, with: 'subscribe').to eq(with: 'subscribe')
      expect { controller.class.on :image, with: 'with' }.to raise_error
    end
  end

  describe 'responder_for' do
    controller do
      wechat_responder
      on :text,  with: 'command', respond: 'string matched'
      on :text,  with: /^cmd:(.*)$/, respond: 'regex matched'
      on :text,  respond: 'text content'
      on :event, with: 'subscribe', respond: 'subscribe event'
      on :click, with: 'EVENTKEY', respond: 'EVENTKEY clicked'
      on :image, respond: 'image content'
    end

    specify 'find first responder for matched type' do
      expect do |b|
        controller.class.responder_for(MsgType: 'image', &b)
      end.to yield_with_args(respond: 'image content')
    end

    specify "find 'general text' responder if none of the text responders matches request content" do
      expect do |b|
        controller.class.responder_for(MsgType: 'text', Content: 'some text', &b)
      end.to yield_with_args({ respond: 'text content' }, 'some text')
    end

    specify "find 'string matched' responder if request content matches string" do
      expect do |b|
        controller.class.responder_for(MsgType: 'text', Content: 'command', &b)
      end.to yield_with_args({ respond: 'string matched', with: 'command' }, 'command')
    end

    specify "find 'regex mached' responder if request content matches regex" do
      expect do |b|
        controller.class.responder_for(MsgType: 'text', Content: 'cmd:my_command', &b)
      end.to yield_with_args({ respond: 'regex matched', with: /^cmd:(.*)$/ }, 'my_command')
    end

    specify "find 'subscribe event' responder if event request matches event" do
      expect do |b|
        controller.class.responder_for(MsgType: 'event', Event: 'subscribe', &b)
      end.to yield_with_args({ respond: 'subscribe event', with: 'subscribe' }, 'subscribe')
    end

    specify "find 'click' responder if event request matches click" do
      expect do |b|
        controller.class.responder_for(MsgType: 'event', Event: 'click', EventKey: 'EVENTKEY', &b)
      end.to yield_with_args({ respond: 'EVENTKEY clicked', with: 'EVENTKEY' }, 'EVENTKEY')
    end
  end

  specify 'will respond empty if no responder for the message type' do
    post :create, signature_params.merge(xml: text_message)
    expect(response.code).to eq('200')
    expect(response.body.strip).to be_empty
  end

  describe 'respond_to wechat helper' do
    controller do
      wechat_responder
      on :text do |_message, _content|
        wechat
      end
    end

    specify 'will return normal' do
      expect do
        post :create, signature_params.merge(xml: text_message)
      end.not_to raise_error
    end
  end

  describe 'respond_to wechat_url helper' do
    controller do
      wechat_responder
      on :text do |_message, _content|
        wechat_url
      end
    end

    specify 'will return normal' do
      expect do
        post :create, signature_params.merge(xml: text_message)
      end.not_to raise_error
    end
  end

  describe 'fallback responder' do
    controller do
      wechat_responder
      on :fallback, respond: 'fallback responder'
    end

    specify 'will respond to any message' do
      post :create, signature_params.merge(xml: text_message)
      expect(xml_to_hash(response)[:Content]).to eq('fallback responder')
    end
  end

  describe 'fallback responder transfer to customer service' do
    controller do
      wechat_responder
      on :fallback do |message|
        message.reply.transfer_customer_service
      end
    end

    specify 'will change MsgType to transfer_customer_service' do
      post :create, signature_params.merge(xml: text_message)
      expect(xml_to_hash(response)[:MsgType]).to eq 'transfer_customer_service'
    end
  end

  describe 'default text transfer to customer service' do
    controller do
      wechat_responder
      on :text do |request, _content|
        request.reply.transfer_customer_service
      end
    end

    specify 'will change MsgType to transfer_customer_service' do
      post :create, signature_params.merge(xml: text_message)
      expect(xml_to_hash(response)[:MsgType]).to eq 'transfer_customer_service'
    end
  end

  describe '#create use cases' do
    controller do
      wechat_responder
      on :text, respond: 'text message' do |message, _content|
        message.replay.text('should not be here')
      end

      on :text, with: 'command' do |message, content|
        message.reply.text("text: #{content}")
      end

      on :text, with: /^cmd:(.*)$/ do |message, cmd|
        message.reply.text("cmd: #{cmd}")
      end

      on :text, with: 'session count' do |message|
        message.session[:count] ||= 0
        message.session[:count] += 1
        message.reply.text message.session[:count]
      end

      on :event, with: 'subscribe' do |message, event|
        message.reply.text("event: #{event}")
      end

      on :event, with: 'unsubscribe' do |message|
        message.reply.success
      end

      on :scan, with: 'qrscene_xxxxxx' do |request, ticket|
        request.reply.text "Unsubscribe user #{request[:FromUserName]} Ticket #{ticket}"
      end

      on :scan, with: 'scene_id' do |request, ticket|
        request.reply.text "Subscribe user #{request[:FromUserName]} Ticket #{ticket}"
      end

      on :event, with: 'scan' do |request|
        if request[:EventKey].present?
          request.reply.text "event scan got EventKey #{request[:EventKey]} Ticket #{request[:Ticket]}"
        end
      end

      on :location do |message|
        message.reply.text("Latitude: #{message[:Latitude]} Longitude: #{message[:Longitude]}")
      end

      on :image do |message|
        message.reply.text("image: #{message[:PicUrl]}")
      end

      on :voice do |message|
        message.reply.text("voice: #{message[:MediaId]}")
      end

      on :video do |message|
        message.reply.text("video: #{message[:MediaId]}")
      end

      on :link do |message|
        message.reply.text("link: #{message[:Url]}")
      end
    end

    specify 'response with respond field' do
      post :create, signature_params.merge(xml: text_message.merge(Content: 'message'))
      result = xml_to_hash(response)
      expect(result[:ToUserName]).to eq('fromUser')
      expect(result[:FromUserName]).to eq('toUser')
      expect(result[:Content]).to eq('text message')
    end

    specify 'response text with text match' do
      post :create, signature_params.merge(xml: text_message.merge(Content: 'command'))
      expect(xml_to_hash(response)[:Content]).to eq('text: command')
    end

    specify 'response text with regex matched' do
      post :create, signature_params.merge(xml: text_message.merge(Content: 'cmd:reload'))
      expect(xml_to_hash(response)[:Content]).to eq('cmd: reload')
    end

    specify 'response text with session count' do
      post :create, signature_params.merge(xml: text_message.merge(Content: 'session count'))
      expect(xml_to_hash(response)[:Content]).to eq('1')
    end

    specify 'response subscribe event with matched event' do
      event_message = message_base.merge(MsgType: 'event', Event: 'subscribe', EventKey: 'qrscene_not_exist')
      post :create, signature_params.merge(xml: event_message)
      expect(xml_to_hash(response)[:Content]).to eq('event: subscribe')
    end

    specify 'response unsubscribe event with matched event' do
      event_message = message_base.merge(MsgType: 'event', Event: 'unsubscribe')
      post :create, signature_params.merge(xml: event_message)
      expect(response.code).to eq('200')
      expect(response.body).to eq('success')
    end

    specify 'response subscribe scan event with matched event' do
      event_message = message_base.merge(MsgType: 'event', Event: 'subscribe', EventKey: 'qrscene_xxxxxx')
      post :create, signature_params.merge(xml: event_message.merge(Ticket: 'TICKET'))
      expect(xml_to_hash(response)[:Content]).to eq 'Unsubscribe user fromUser Ticket TICKET'
    end

    specify 'response scan event with matched event' do
      event_message = message_base.merge(MsgType: 'event', Event: 'SCAN', EventKey: 'scene_id')
      post :create, signature_params.merge(xml: event_message.merge(Ticket: 'TICKET'))
      expect(xml_to_hash(response)[:Content]).to eq 'Subscribe user fromUser Ticket TICKET'
    end

    specify 'response scan event with by_passed scene_id' do
      event_message = message_base.merge(MsgType: 'event', Event: 'SCAN', EventKey: 'scene_id_by_pass_scan_process')
      post :create, signature_params.merge(xml: event_message.merge(Ticket: 'TICKET'))
      expect(xml_to_hash(response)[:Content]).to eq 'event scan got EventKey scene_id_by_pass_scan_process Ticket TICKET'
    end

    specify 'response location' do
      message = message_base.merge(MsgType: 'event', Event: 'LOCATION', Latitude: 23.137466, Longitude: 113.352425, Precision: 119.385040)
      post :create, signature_params.merge(xml: message)
      expect(xml_to_hash(response)[:Content]).to eq('Latitude: 23.137466 Longitude: 113.352425')
    end

    specify 'response image' do
      image_message = message_base.merge(MsgType: 'image', MediaId: 'image_media_id', PicUrl: 'pic_url')
      post :create, signature_params.merge(xml: image_message)
      expect(xml_to_hash(response)[:Content]).to eq('image: pic_url')
    end

    specify 'response voice' do
      message = message_base.merge(MsgType: 'voice', MediaId: 'voice_media_id', Format: 'format')
      post :create, signature_params.merge(xml: message)
      expect(xml_to_hash(response)[:Content]).to eq('voice: voice_media_id')
    end

    specify 'response video' do
      message = message_base.merge(MsgType: 'video', MediaId: 'video_media_id', ThumbMediaId: 'thumb_media_id')
      post :create, signature_params.merge(xml: message)
      expect(xml_to_hash(response)[:Content]).to eq('video: video_media_id')
    end

    specify 'response link' do
      message = message_base.merge(MsgType: 'link', Url: 'link_url', Title: 'title', Description: 'description')
      post :create, signature_params.merge(xml: message)
      expect(xml_to_hash(response)[:Content]).to eq('link: link_url')
    end
  end
end
