defmodule Mv.Authorization.Checks.HasPermissionTest do
  @moduledoc """
  Tests for the HasPermission Ash Policy Check.

  This check evaluates permissions from the PermissionSets module and applies
  scope filters to Ash queries.
  """
  use ExUnit.Case, async: true

  alias Mv.Authorization.Checks.HasPermission

  # Helper to create a mock authorizer for strict_check/3
  defp create_authorizer(resource, action) do
    %Ash.Policy.Authorizer{
      resource: resource,
      subject: %{
        action: %{type: action},
        data: nil
      }
    }
  end

  # Helper to create actor with role
  defp create_actor(id, permission_set_name, opts \\ []) do
    actor = %{
      id: id,
      role: %{permission_set_name: permission_set_name}
    }

    # Add member_id if provided (needed for :linked scope tests)
    case Keyword.get(opts, :member_id) do
      nil -> actor
      member_id -> Map.put(actor, :member_id, member_id)
    end
  end

  describe "describe/1" do
    test "returns human-readable description" do
      description = HasPermission.describe([])
      assert is_binary(description)
      assert description =~ "permission"
    end
  end

  describe "strict_check/3 - Permission Lookup" do
    test "admin has permission for all resources/actions" do
      admin = create_actor("admin-123", "admin")
      authorizer = create_authorizer(Mv.Membership.Member, :read)

      {:ok, result} = HasPermission.strict_check(admin, authorizer, [])

      assert result == true or result == :unknown
    end

    test "read_only has read permission for Member" do
      read_only_user = create_actor("read-only-123", "read_only")
      authorizer = create_authorizer(Mv.Membership.Member, :read)

      {:ok, result} = HasPermission.strict_check(read_only_user, authorizer, [])

      assert result == true or result == :unknown
    end

    test "read_only does NOT have create permission for Member" do
      read_only_user = create_actor("read-only-123", "read_only")
      authorizer = create_authorizer(Mv.Membership.Member, :create)

      {:ok, result} = HasPermission.strict_check(read_only_user, authorizer, [])

      assert result == false
    end

    test "own_data has update permission for User with scope :own" do
      own_data_user = create_actor("user-123", "own_data")
      authorizer = create_authorizer(Mv.Accounts.User, :update)

      {:ok, result} = HasPermission.strict_check(own_data_user, authorizer, [])

      # Should return false for :own scope without record
      # This prevents bypassing expr-based filters in bypass policies
      # The actual filtering is done via bypass policies with expr(id == ^actor(:id))
      assert result == false
    end
  end

  describe "strict_check/3 - Scope :all" do
    test "actor with scope :all can access any record" do
      admin = create_actor("admin-123", "admin")
      authorizer = create_authorizer(Mv.Membership.Member, :read)

      {:ok, result} = HasPermission.strict_check(admin, authorizer, [])

      # :all scope should return true (no filter needed)
      assert result == true
    end

    test "admin can read all members without filter" do
      admin = create_actor("admin-123", "admin")
      authorizer = create_authorizer(Mv.Membership.Member, :read)

      {:ok, result} = HasPermission.strict_check(admin, authorizer, [])

      # Should return true for :all scope
      assert result == true
    end
  end

  describe "strict_check/3 - Scope :own" do
    test "actor with scope :own returns false (needs bypass policy with expr filter)" do
      user = create_actor("user-123", "own_data")
      authorizer = create_authorizer(Mv.Accounts.User, :read)

      {:ok, result} = HasPermission.strict_check(user, authorizer, [])

      # Should return false for :own scope without record
      # This prevents bypassing expr-based filters in bypass policies
      # The actual filtering is done via bypass policies with expr(id == ^actor(:id))
      assert result == false
    end
  end

  describe "auto_filter/3 - Scope :own" do
    test "scope :own returns filter expression" do
      user = create_actor("user-123", "own_data")
      authorizer = create_authorizer(Mv.Accounts.User, :update)

      filter = HasPermission.auto_filter(user, authorizer, [])

      # Should return a filter expression
      assert not is_nil(filter)
    end
  end

  describe "auto_filter/3 - Scope :linked" do
    test "scope :linked for Member returns user_id filter" do
      user = create_actor("user-123", "own_data", member_id: "member-456")
      authorizer = create_authorizer(Mv.Membership.Member, :read)

      filter = HasPermission.auto_filter(user, authorizer, [])

      # Should return a filter expression
      assert not is_nil(filter)
    end

    test "scope :linked for CustomFieldValue returns member.user_id filter" do
      user = create_actor("user-123", "own_data", member_id: "member-456")
      authorizer = create_authorizer(Mv.Membership.CustomFieldValue, :update)

      filter = HasPermission.auto_filter(user, authorizer, [])

      # Should return a filter expression that traverses member relationship
      assert not is_nil(filter)
    end
  end

  describe "strict_check/3 - Error Handling" do
    test "returns {:ok, false} for nil actor" do
      authorizer = create_authorizer(Mv.Membership.Member, :read)

      {:ok, result} = HasPermission.strict_check(nil, authorizer, [])

      assert result == false
    end

    test "returns {:ok, false} for actor missing role" do
      actor_without_role = %{id: "user-123"}
      authorizer = create_authorizer(Mv.Membership.Member, :read)

      {:ok, result} = HasPermission.strict_check(actor_without_role, authorizer, [])

      assert result == false
    end

    test "returns {:ok, false} for actor with nil role" do
      actor_with_nil_role = %{id: "user-123", role: nil}
      authorizer = create_authorizer(Mv.Membership.Member, :read)

      {:ok, result} = HasPermission.strict_check(actor_with_nil_role, authorizer, [])

      assert result == false
    end

    test "returns {:ok, false} for invalid permission_set_name" do
      actor_with_invalid_permission = %{
        id: "user-123",
        role: %{permission_set_name: "invalid_set"}
      }

      authorizer = create_authorizer(Mv.Membership.Member, :read)

      {:ok, result} = HasPermission.strict_check(actor_with_invalid_permission, authorizer, [])

      assert result == false
    end

    test "returns {:ok, false} for no matching permission" do
      read_only_user = create_actor("read-only-123", "read_only")
      authorizer = create_authorizer(Mv.Authorization.Role, :create)

      {:ok, result} = HasPermission.strict_check(read_only_user, authorizer, [])

      assert result == false
    end

    test "handles role with nil permission_set_name gracefully" do
      actor_with_nil_permission_set = %{
        id: "user-123",
        role: %{permission_set_name: nil}
      }

      authorizer = create_authorizer(Mv.Membership.Member, :read)

      {:ok, result} = HasPermission.strict_check(actor_with_nil_permission_set, authorizer, [])

      assert result == false
    end
  end

  describe "strict_check/3 - Logging" do
    import ExUnit.CaptureLog

    test "logs authorization failure for nil actor" do
      authorizer = create_authorizer(Mv.Membership.Member, :read)

      log =
        capture_log(fn ->
          HasPermission.strict_check(nil, authorizer, [])
        end)

      assert log =~ "Authorization failed" or log == ""
    end

    test "logs authorization failure for missing role" do
      actor_without_role = %{id: "user-123"}
      authorizer = create_authorizer(Mv.Membership.Member, :read)

      log =
        capture_log(fn ->
          HasPermission.strict_check(actor_without_role, authorizer, [])
        end)

      assert log =~ "Authorization failed" or log == ""
    end
  end

  describe "strict_check/3 - Resource Name Extraction" do
    test "correctly extracts resource name from nested module" do
      admin = create_actor("admin-123", "admin")
      authorizer = create_authorizer(Mv.Membership.Member, :read)

      {:ok, result} = HasPermission.strict_check(admin, authorizer, [])

      # Should work correctly (not crash)
      assert result == true or result == :unknown or result == false
    end

    test "works with different resource modules" do
      admin = create_actor("admin-123", "admin")

      resources = [
        Mv.Accounts.User,
        Mv.Membership.Member,
        Mv.Membership.CustomFieldValue,
        Mv.Membership.CustomField,
        Mv.Authorization.Role
      ]

      for resource <- resources do
        authorizer = create_authorizer(resource, :read)
        {:ok, result} = HasPermission.strict_check(admin, authorizer, [])

        # Should not crash and should return valid result
        assert result == true or result == :unknown or result == false
      end
    end
  end

  describe "strict_check/3 - Role Loading Fallback" do
    test "returns false if role is NotLoaded and cannot be loaded" do
      # Create actor with NotLoaded role
      # In real scenario, ensure_role_loaded would attempt to load via Ash.load
      # For this test, we use a simple map to verify the pattern matching works
      actor = %{
        id: "user-123",
        role: %Ash.NotLoaded{}
      }

      authorizer = create_authorizer(Mv.Accounts.User, :read)

      # Should handle NotLoaded pattern and return false
      # (In real scenario, ensure_role_loaded would attempt to load, but for this test
      # we just verify the pattern matching works correctly)
      {:ok, result} = HasPermission.strict_check(actor, authorizer, [])
      assert result == false
    end

    test "returns false if role is nil" do
      actor = %{
        id: "user-123",
        role: nil
      }

      authorizer = create_authorizer(Mv.Accounts.User, :read)

      {:ok, result} = HasPermission.strict_check(actor, authorizer, [])
      assert result == false
    end

    test "works correctly when role is already loaded" do
      actor = create_actor("user-123", "admin")
      authorizer = create_authorizer(Mv.Accounts.User, :read)

      {:ok, result} = HasPermission.strict_check(actor, authorizer, [])
      assert result == true
    end
  end
end
