defmodule PhoenixLiveState.StateServer.PolicyTest do
  use ExUnit.Case

  alias PhoenixLiveState.StateServer.InternalState
  alias PhoenixLiveState.StateServer.Policy
  alias PhoenixLiveState.StateServer.Settings

  describe "build_acl/1" do
    test "returns valid ACL with empty lists when no actions specified" do
      assert {:ok, %{group: [], public: []}} = Policy.build_acl(%{})
    end

    test "returns valid ACL with :all for both client types" do
      acl_candidate = %{group: :all, public: :all}
      assert {:ok, %{group: :all, public: :all}} = Policy.build_acl(acl_candidate)
    end

    test "returns valid ACL with specific actions for group and public" do
      acl_candidate = %{group: [:read, :write], public: [:read]}
      assert {:ok, %{group: [:read, :write], public: [:read]}} = Policy.build_acl(acl_candidate)
    end

    test "returns valid ACL with all allowed actions" do
      acl_candidate = %{
        group: [:read, :write, :delete, :reset],
        public: [:read, :write, :delete, :reset]
      }

      expected = %{
        group: [:read, :write, :delete, :reset],
        public: [:read, :write, :delete, :reset]
      }

      assert {:ok, ^expected} = Policy.build_acl(acl_candidate)
    end

    test "returns valid ACL when only group is specified" do
      acl_candidate = %{group: [:read, :write]}
      assert {:ok, %{group: [:read, :write], public: []}} = Policy.build_acl(acl_candidate)
    end

    test "returns valid ACL when only public is specified" do
      acl_candidate = %{public: [:read]}
      assert {:ok, %{group: [], public: [:read]}} = Policy.build_acl(acl_candidate)
    end

    test "returns valid ACL mixing :all and specific actions" do
      acl_candidate = %{group: :all, public: [:read]}
      assert {:ok, %{group: :all, public: [:read]}} = Policy.build_acl(acl_candidate)
    end

    test "returns error when group has invalid action" do
      acl_candidate = %{group: [:read, :invalid_action], public: [:read]}
      assert {:error, :invalid_acl} = Policy.build_acl(acl_candidate)
    end

    test "returns error when public has invalid action" do
      acl_candidate = %{group: [:read], public: [:read, :invalid_action]}
      assert {:error, :invalid_acl} = Policy.build_acl(acl_candidate)
    end

    test "returns error when actions is not a list or :all" do
      acl_candidate = %{group: "invalid", public: [:read]}
      assert {:error, :invalid_acl} = Policy.build_acl(acl_candidate)
    end

    test "returns error when actions contains non-atom values" do
      acl_candidate = %{group: [:read, "write"], public: [:read]}
      assert {:error, :invalid_acl} = Policy.build_acl(acl_candidate)
    end

    test "accepts keyword list input and converts to map" do
      acl_candidate = [group: [:read, :write], public: [:read]]
      assert {:ok, %{group: [:read, :write], public: [:read]}} = Policy.build_acl(acl_candidate)
    end

    test "accepts struct or other Access-able input" do
      # Using a simple map to test Access behavior since build_acl calls Map.new/1
      # which handles any Access-compatible structure
      acl_candidate = %{group: [:read], public: [:read]}
      assert {:ok, %{group: [:read], public: [:read]}} = Policy.build_acl(acl_candidate)
    end

    test "handles empty action lists" do
      acl_candidate = %{group: [], public: []}
      assert {:ok, %{group: [], public: []}} = Policy.build_acl(acl_candidate)
    end

    test "returns error when group actions contains duplicates of invalid actions" do
      acl_candidate = %{group: [:read, :invalid, :invalid], public: [:read]}
      assert {:error, :invalid_acl} = Policy.build_acl(acl_candidate)
    end
  end

  describe "default_acl/0" do
    test "returns the expected default ACL" do
      assert Policy.default_acl() == %{
               group: [:read, :write, :delete, :reset],
               public: [:read]
             }
    end
  end

  describe "allowed?/3" do
    test "allows group client to perform allowed actions" do
      client_pid = self()
      clients = MapSet.new([client_pid])
      acl = %{group: [:read, :write], public: [:read]}
      settings = %Settings{acl: acl}
      internal_state = %InternalState{clients: clients, settings: settings}

      assert Policy.allowed?(client_pid, :read, internal_state)
      assert Policy.allowed?(client_pid, :write, internal_state)
    end

    test "denies group client to perform disallowed actions" do
      client_pid = self()
      clients = MapSet.new([client_pid])
      acl = %{group: [:read], public: [:read]}
      settings = %Settings{acl: acl}
      internal_state = %InternalState{clients: clients, settings: settings}

      assert Policy.allowed?(client_pid, :read, internal_state)
      refute Policy.allowed?(client_pid, :write, internal_state)
    end

    test "allows public client to perform allowed actions" do
      client_pid = self()
      other_pid = spawn(fn -> :ok end)
      clients = MapSet.new([other_pid])
      acl = %{group: [:read, :write], public: [:read]}
      settings = %Settings{acl: acl}
      internal_state = %InternalState{clients: clients, settings: settings}

      assert Policy.allowed?(client_pid, :read, internal_state)
    end

    test "denies public client to perform disallowed actions" do
      client_pid = self()
      other_pid = spawn(fn -> :ok end)
      clients = MapSet.new([other_pid])
      acl = %{group: [:read, :write], public: [:read]}
      settings = %Settings{acl: acl}
      internal_state = %InternalState{clients: clients, settings: settings}

      refute Policy.allowed?(client_pid, :write, internal_state)
    end

    test "allows all actions when acl is :all" do
      client_pid = self()
      clients = MapSet.new()
      acl = %{group: :all, public: :all}
      settings = %Settings{acl: acl}
      internal_state = %InternalState{clients: clients, settings: settings}

      assert Policy.allowed?(client_pid, :read, internal_state)
      assert Policy.allowed?(client_pid, :write, internal_state)
      assert Policy.allowed?(client_pid, :delete, internal_state)
      assert Policy.allowed?(client_pid, :reset, internal_state)
    end
  end
end
