# frozen_string_literal: true

require "test_helper"
require "minitest/mock"
require "stubs/test_connection"
require "stubs/room"

class ActionCable::Channel::BaseTest < ActionCable::TestCase
  class ActionCable::Channel::Base
    def kick
      @last_action = [ :kick ]
    end

    def topic
    end
  end

  class BasicChannel < ActionCable::Channel::Base
    def chatters
      @last_action = [ :chatters ]
    end
  end

  class ChatChannel < BasicChannel
    attr_reader :room, :last_action
    after_subscribe :toggle_subscribed
    after_unsubscribe :toggle_subscribed

    class SomeCustomError < StandardError; end
    rescue_from SomeCustomError, with: :error_handler

    def initialize(*)
      @subscribed = false
      super
    end

    def subscribed
      @room = Room.new params[:id]
      @actions = []
    end

    def unsubscribed
      @room = nil
    end

    def toggle_subscribed
      @subscribed = !@subscribed
    end

    def leave
      @last_action = [ :leave ]
    end

    def speak(data)
      @last_action = [ :speak, data ]
    end

    def topic(data)
      @last_action = [ :topic, data ]
    end

    def subscribed?
      @subscribed
    end

    def get_latest
      transmit({ data: "latest" })
    end

    def receive
      @last_action = [ :receive ]
    end

    def error_action
      raise SomeCustomError
    end

    private
      def rm_rf
        @last_action = [ :rm_rf ]
      end

      def error_handler
        @last_action = [ :error_action ]
      end
  end

  setup do
    @user = User.new "lifo"
    @connection = TestConnection.new(@user)
    @channel = ChatChannel.new @connection, "{id: 1}", id: 1
  end

  test "should subscribe to a channel" do
    @channel.subscribe_to_channel
    assert_equal 1, @channel.room.id
  end

  test "on subscribe callbacks" do
    @channel.subscribe_to_channel
    assert @channel.subscribed
  end

  test "channel params" do
    assert_equal({ id: 1 }, @channel.params)
  end

  test "unsubscribing from a channel" do
    @channel.subscribe_to_channel

    assert @channel.room
    assert_predicate @channel, :subscribed?

    @channel.unsubscribe_from_channel

    assert_not @channel.room
    assert_not_predicate @channel, :subscribed?
  end

  test "connection identifiers" do
    assert_equal @user.name, @channel.current_user.name
  end

  test "callable action without any argument" do
    @channel.perform_action "action" => :leave
    assert_equal [ :leave ], @channel.last_action
  end

  test "callable action with arguments" do
    data = { "action" => :speak, "content" => "Hello World" }

    @channel.perform_action data
    assert_equal [ :speak, data ], @channel.last_action
  end

  test "should not dispatch a private method" do
    @channel.perform_action "action" => :rm_rf
    assert_nil @channel.last_action
  end

  test "should not dispatch a public method defined on Base" do
    @channel.perform_action "action" => :kick
    assert_nil @channel.last_action
  end

  test "should dispatch a public method defined on Base and redefined on channel" do
    data = { "action" => :topic, "content" => "This is Sparta!" }

    @channel.perform_action data
    assert_equal [ :topic, data ], @channel.last_action
  end

  test "should dispatch calling a public method defined in an ancestor" do
    @channel.perform_action "action" => :chatters
    assert_equal [ :chatters ], @channel.last_action
  end

  test "should dispatch receive action when perform_action is called with empty action" do
    data = { "content" => "hello" }
    @channel.perform_action data
    assert_equal [ :receive ], @channel.last_action
  end

  test "transmitting data" do
    @channel.perform_action "action" => :get_latest

    expected = { "identifier" => "{id: 1}", "message" => { "data" => "latest" } }
    assert_equal expected, @connection.last_transmission
  end

  test "do not send subscription confirmation on initialize" do
    assert_nil @connection.last_transmission
  end

  test "subscription confirmation on subscribe_to_channel" do
    expected = { "identifier" => "{id: 1}", "type" => "confirm_subscription" }
    @channel.subscribe_to_channel
    assert_equal expected, @connection.last_transmission
  end

  test "actions available on Channel" do
    available_actions = %w(room last_action subscribed unsubscribed toggle_subscribed leave speak subscribed? get_latest receive chatters topic error_action).to_set
    assert_equal available_actions, ChatChannel.action_methods
  end

  test "invalid action on Channel" do
    assert_logged("Unable to process ActionCable::Channel::BaseTest::ChatChannel#invalid_action") do
      @channel.perform_action "action" => :invalid_action
    end
  end

  test "notification for perform_action" do
    events = []
    ActiveSupport::Notifications.subscribe "perform_action.action_cable" do |*args|
      events << ActiveSupport::Notifications::Event.new(*args)
    end

    data = { "action" => :speak, "content" => "hello" }
    @channel.perform_action data

    assert_equal 1, events.length
    assert_equal "perform_action.action_cable", events[0].name
    assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class]
    assert_equal :speak, events[0].payload[:action]
    assert_equal data, events[0].payload[:data]
  ensure
    ActiveSupport::Notifications.unsubscribe "perform_action.action_cable"
  end

  test "notification for transmit" do
    events = []
    ActiveSupport::Notifications.subscribe "transmit.action_cable" do |*args|
      events << ActiveSupport::Notifications::Event.new(*args)
    end

    @channel.perform_action "action" => :get_latest
    expected_data = { data: "latest" }

    assert_equal 1, events.length
    assert_equal "transmit.action_cable", events[0].name
    assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class]
    assert_equal expected_data, events[0].payload[:data]
    assert_nil events[0].payload[:via]
  ensure
    ActiveSupport::Notifications.unsubscribe "transmit.action_cable"
  end

  test "notification for transmit_subscription_confirmation" do
    @channel.subscribe_to_channel

    events = []
    ActiveSupport::Notifications.subscribe "transmit_subscription_confirmation.action_cable" do |*args|
      events << ActiveSupport::Notifications::Event.new(*args)
    end

    @channel.stub(:subscription_confirmation_sent?, false) do
      @channel.send(:transmit_subscription_confirmation)

      assert_equal 1, events.length
      assert_equal "transmit_subscription_confirmation.action_cable", events[0].name
      assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class]
    end
  ensure
    ActiveSupport::Notifications.unsubscribe "transmit_subscription_confirmation.action_cable"
  end

  test "notification for transmit_subscription_rejection" do
    events = []
    ActiveSupport::Notifications.subscribe "transmit_subscription_rejection.action_cable" do |*args|
      events << ActiveSupport::Notifications::Event.new(*args)
    end

    @channel.send(:transmit_subscription_rejection)

    assert_equal 1, events.length
    assert_equal "transmit_subscription_rejection.action_cable", events[0].name
    assert_equal "ActionCable::Channel::BaseTest::ChatChannel", events[0].payload[:channel_class]
  ensure
    ActiveSupport::Notifications.unsubscribe "transmit_subscription_rejection.action_cable"
  end

  test "behaves like rescuable" do
    @channel.perform_action "action" => :error_action
    assert_equal [ :error_action ], @channel.last_action
  end

  private
    def assert_logged(message)
      old_logger = @connection.logger
      log = StringIO.new
      @connection.instance_variable_set(:@logger, Logger.new(log))

      begin
        yield

        log.rewind
        assert_match message, log.read
      ensure
        @connection.instance_variable_set(:@logger, old_logger)
      end
    end
end
