# frozen_string_literal: true

require "presence_channel"

RSpec.describe PresenceChannel do
  fab!(:user)
  fab!(:group) { Fabricate(:group).tap { |g| g.add(user) } }
  fab!(:user2) { Fabricate(:user) }

  before do
    PresenceChannel.clear_all!

    secure_user = Fabricate(:user)
    secure_group = Fabricate(:group)
    PresenceChannel.register_prefix("test") do |channel|
      case channel
      when %r{\A/test/public\d*\z}
        PresenceChannel::Config.new(public: true)
      when "/test/secureuser"
        PresenceChannel::Config.new(allowed_user_ids: [secure_user.id])
      when "/test/securegroup"
        PresenceChannel::Config.new(allowed_group_ids: [secure_group.id])
      when "/test/alloweduser"
        PresenceChannel::Config.new(allowed_user_ids: [user.id])
      when "/test/allowedgroup"
        PresenceChannel::Config.new(allowed_group_ids: [group.id])
      when "/test/everyonegroup"
        PresenceChannel::Config.new(allowed_group_ids: [Group::AUTO_GROUPS[:everyone]])
      when "/test/noaccess"
        PresenceChannel::Config.new
      when "/test/countonly"
        PresenceChannel::Config.new(count_only: true, public: true)
      else
        nil
      end
    end
  end

  after do
    PresenceChannel.clear_all!
    PresenceChannel.unregister_prefix("test")
  end

  it "can perform basic channel functionality" do
    channel1 = PresenceChannel.new("/test/public1")
    channel2 = PresenceChannel.new("/test/public1")
    channel3 = PresenceChannel.new("/test/public1")

    expect(channel3.user_ids).to eq([])

    channel1.present(user_id: user.id, client_id: 1)
    channel2.present(user_id: user.id, client_id: 2)

    expect(channel3.user_ids).to eq([user.id])
    expect(channel3.count).to eq(1)

    channel1.leave(user_id: user.id, client_id: 2)

    expect(channel3.user_ids).to eq([user.id])
    expect(channel3.count).to eq(1)

    channel2.leave(user_id: user.id, client_id: 1)

    expect(channel3.user_ids).to eq([])
    expect(channel3.count).to eq(0)
  end

  it "does not raise error when getting channel config under readonly" do
    PresenceChannel.redis.stubs(:set).raises(Redis::CommandError.new("READONLY")).once
    channel = PresenceChannel.new("/test/public1")
    expect(channel.user_ids).to eq([])
  end

  it "can automatically expire users" do
    channel = PresenceChannel.new("/test/public1")

    channel.present(user_id: user.id, client_id: 76)
    channel.present(user_id: user.id, client_id: 77)

    expect(channel.count).to eq(1)

    freeze_time Time.zone.now + 1 + PresenceChannel::DEFAULT_TIMEOUT

    Jobs::PresenceChannelAutoLeave.new.execute({})

    expect(channel.count).to eq(0)
  end

  it "correctly sends messages to message bus" do
    channel = PresenceChannel.new("/test/public1")

    messages =
      MessageBus.track_publish(channel.message_bus_channel_name) do
        channel.present(user_id: user.id, client_id: "a")
      end

    data = messages.map(&:data)
    expect(data.count).to eq(1)
    expect(data[0].keys).to contain_exactly("entering_users")
    expect(data[0]["entering_users"].map { |u| u[:id] }).to contain_exactly(user.id)

    freeze_time Time.zone.now + 1 + PresenceChannel::DEFAULT_TIMEOUT

    messages = MessageBus.track_publish(channel.message_bus_channel_name) { channel.auto_leave }

    data = messages.map(&:data)
    expect(data.count).to eq(1)
    expect(data[0].keys).to contain_exactly("leaving_user_ids")
    expect(data[0]["leaving_user_ids"]).to contain_exactly(user.id)
  end

  it "can track active channels, and auto_leave_all successfully" do
    channel1 = PresenceChannel.new("/test/public1")
    channel2 = PresenceChannel.new("/test/public2")

    channel1.present(user_id: user.id, client_id: "a")
    channel2.present(user_id: user.id, client_id: "a")

    start_time = Time.zone.now

    freeze_time start_time + PresenceChannel::DEFAULT_TIMEOUT / 2

    channel2.present(user_id: user2.id, client_id: "b")

    freeze_time start_time + PresenceChannel::DEFAULT_TIMEOUT + 1

    messages = MessageBus.track_publish { PresenceChannel.auto_leave_all }

    expect(messages.map { |m| [m.channel, m.data] }).to contain_exactly(
      ["/presence/test/public1", { "leaving_user_ids" => [user.id] }],
      ["/presence/test/public2", { "leaving_user_ids" => [user.id] }],
    )

    expect(channel1.user_ids).to eq([])
    expect(channel2.user_ids).to eq([user2.id])
  end

  it "only sends one `enter` and `leave` message" do
    channel = PresenceChannel.new("/test/public1")

    messages =
      MessageBus.track_publish(channel.message_bus_channel_name) do
        channel.present(user_id: user.id, client_id: "a")
        channel.present(user_id: user.id, client_id: "a")
        channel.present(user_id: user.id, client_id: "b")
      end

    data = messages.map(&:data)
    expect(data.count).to eq(1)
    expect(data[0].keys).to contain_exactly("entering_users")
    expect(data[0]["entering_users"].map { |u| u[:id] }).to contain_exactly(user.id)

    messages =
      MessageBus.track_publish(channel.message_bus_channel_name) do
        channel.leave(user_id: user.id, client_id: "a")
        channel.leave(user_id: user.id, client_id: "a")
        channel.leave(user_id: user.id, client_id: "b")
      end

    data = messages.map(&:data)
    expect(data.count).to eq(1)
    expect(data[0].keys).to contain_exactly("leaving_user_ids")
    expect(data[0]["leaving_user_ids"]).to contain_exactly(user.id)
  end

  it "will return the messagebus last_id in the state payload" do
    channel = PresenceChannel.new("/test/public1")

    channel.present(user_id: user.id, client_id: "a")
    channel.present(user_id: user2.id, client_id: "a")

    state = channel.state
    expect(state.user_ids).to contain_exactly(user.id, user2.id)
    expect(state.count).to eq(2)
    expect(state.message_bus_last_id).to eq(MessageBus.last_id(channel.message_bus_channel_name))
  end

  it "sets an expiry on all channel-specific keys" do
    r = Discourse.redis.without_namespace
    channel = PresenceChannel.new("/test/public1")
    channel.present(user_id: user.id, client_id: "a")

    channels_ttl = r.ttl(PresenceChannel.redis_key_channel_list)
    expect(channels_ttl).to eq(-1) # Persistent

    initial_zlist_ttl = r.ttl(channel.send(:redis_key_zlist))
    initial_hash_ttl = r.ttl(channel.send(:redis_key_hash))

    expect(initial_zlist_ttl).to be_between(
      PresenceChannel::GC_SECONDS,
      PresenceChannel::GC_SECONDS + 5.minutes,
    )
    expect(initial_hash_ttl).to be_between(
      PresenceChannel::GC_SECONDS,
      PresenceChannel::GC_SECONDS + 5.minutes,
    )

    freeze_time 1.minute.from_now

    # PresenceChannel#present is responsible for bumping ttl
    channel.present(user_id: user.id, client_id: "a")

    new_zlist_ttl = r.ttl(channel.send(:redis_key_zlist))
    new_hash_ttl = r.ttl(channel.send(:redis_key_hash))

    expect(new_zlist_ttl).to be > initial_zlist_ttl
    expect(new_hash_ttl).to be > initial_hash_ttl
  end

  it "handles security correctly for anon" do
    expect(PresenceChannel.new("/test/public1").can_enter?(user_id: nil)).to eq(false)
    expect(PresenceChannel.new("/test/secureuser").can_enter?(user_id: nil)).to eq(false)
    expect(PresenceChannel.new("/test/securegroup").can_enter?(user_id: nil)).to eq(false)
    expect(PresenceChannel.new("/test/noaccess").can_enter?(user_id: nil)).to eq(false)
    expect(PresenceChannel.new("/test/everyonegroup").can_enter?(user_id: nil)).to eq(false)

    expect(PresenceChannel.new("/test/public1").can_view?(user_id: nil)).to eq(true)
    expect(PresenceChannel.new("/test/secureuser").can_view?(user_id: nil)).to eq(false)
    expect(PresenceChannel.new("/test/securegroup").can_view?(user_id: nil)).to eq(false)
    expect(PresenceChannel.new("/test/noaccess").can_view?(user_id: nil)).to eq(false)
    expect(PresenceChannel.new("/test/everyonegroup").can_view?(user_id: nil)).to eq(false)
  end

  it "handles security correctly for a user" do
    expect(PresenceChannel.new("/test/secureuser").can_enter?(user_id: user.id)).to eq(false)
    expect(PresenceChannel.new("/test/securegroup").can_enter?(user_id: user.id)).to eq(false)
    expect(PresenceChannel.new("/test/alloweduser").can_enter?(user_id: user.id)).to eq(true)
    expect(PresenceChannel.new("/test/allowedgroup").can_enter?(user_id: user.id)).to eq(true)
    expect(PresenceChannel.new("/test/everyonegroup").can_enter?(user_id: user.id)).to eq(true)
    expect(PresenceChannel.new("/test/noaccess").can_enter?(user_id: user.id)).to eq(false)

    expect(PresenceChannel.new("/test/secureuser").can_view?(user_id: user.id)).to eq(false)
    expect(PresenceChannel.new("/test/securegroup").can_view?(user_id: user.id)).to eq(false)
    expect(PresenceChannel.new("/test/alloweduser").can_view?(user_id: user.id)).to eq(true)
    expect(PresenceChannel.new("/test/allowedgroup").can_view?(user_id: user.id)).to eq(true)
    expect(PresenceChannel.new("/test/everyonegroup").can_view?(user_id: user.id)).to eq(true)
    expect(PresenceChannel.new("/test/noaccess").can_view?(user_id: user.id)).to eq(false)
  end

  it "publishes messages with appropriate security" do
    channel = PresenceChannel.new("/test/alloweduser")
    messages =
      MessageBus.track_publish(channel.message_bus_channel_name) do
        channel.present(user_id: user.id, client_id: "a")
      end
    expect(messages.count).to eq(1)
    expect(messages[0].user_ids).to eq([user.id])

    channel = PresenceChannel.new("/test/allowedgroup")
    messages =
      MessageBus.track_publish(channel.message_bus_channel_name) do
        channel.present(user_id: user.id, client_id: "a")
      end
    expect(messages.count).to eq(1)
    expect(messages[0].group_ids).to eq([group.id])
  end

  it "publishes messages correctly in count_only mode" do
    channel = PresenceChannel.new("/test/countonly")
    messages =
      MessageBus.track_publish(channel.message_bus_channel_name) do
        channel.present(user_id: user.id, client_id: "a")
      end
    expect(messages.count).to eq(1)
    expect(messages[0].data).to eq({ "count_delta" => 1 })

    messages =
      MessageBus.track_publish(channel.message_bus_channel_name) do
        channel.leave(user_id: user.id, client_id: "a")
      end
    expect(messages.count).to eq(1)
    expect(messages[0].data).to eq({ "count_delta" => -1 })
  end

  it "sets a mutex when the change involves publishing messages" do
    channel = PresenceChannel.new("/test/public1")

    messages_published = 0
    channel.define_singleton_method(:publish_message) do |*args, **kwargs|
      val = PresenceChannel.redis.get(redis_key_mutex)
      raise "Mutex was not set" if val.nil?
      messages_published += 1
    end

    redis_key_mutex = Discourse.redis.namespace_key("_presence_/test/public1_mutex")

    # Enter and leave
    expect(PresenceChannel.redis.get(redis_key_mutex)).to eq(nil)
    channel.present(user_id: user.id, client_id: "a")
    expect(PresenceChannel.redis.get(redis_key_mutex)).to eq(nil)
    channel.leave(user_id: user.id, client_id: "a")
    expect(PresenceChannel.redis.get(redis_key_mutex)).to eq(nil)
    expect(messages_published).to eq(2)

    # Enter and auto_leave
    channel.present(user_id: user.id, client_id: "a")
    expect(PresenceChannel.redis.get(redis_key_mutex)).to eq(nil)
    freeze_time 1.hour.from_now
    channel.auto_leave
    expect(PresenceChannel.redis.get(redis_key_mutex)).to eq(nil)

    expect(messages_published).to eq(4)
  end
end