defmodule Ash.Test.Resource.CalculationsTest do
  @moduledoc false
  use ExUnit.Case, async: true

  alias Ash.Resource.Calculation
  alias Ash.Test.Support.PolicySimple.Domain
  alias Ash.Test.Support.PolicySimple.Post

  alias Ash.Test.Domain, as: Domain

  defmacrop defposts(do: body) do
    module = Module.concat(["rand#{System.unique_integer([:positive])}", Post])

    quote do
      defmodule unquote(module) do
        @moduledoc false
        use Ash.Resource, domain: Domain

        attributes do
          uuid_primary_key :id

          attribute :name, :string do
            public?(true)
          end

          attribute :contents, :string do
            public?(true)
          end
        end

        unquote(body)
      end

      alias unquote(module), as: Post
    end
  end

  describe "representation" do
    test "calculations are persisted on the resource properly" do
      defposts do
        calculations do
          calculate :name_and_contents, :string, concat([:name, :context]) do
            public?(true)
          end

          calculate :another_cal_but_private, :string, concat([:name, :context])
        end
      end

      assert [
               %Calculation{
                 name: :name_and_contents,
                 calculation: {Calculation.Concat, [keys: [:name, :context], separator: ""]},
                 public?: true
               },
               %Calculation{
                 name: :another_cal_but_private,
                 calculation: {Calculation.Concat, [keys: [:name, :context], separator: ""]},
                 public?: false
               }
             ] = Ash.Resource.Info.calculations(Post)

      assert [%Calculation{name: :name_and_contents}] =
               Ash.Resource.Info.public_calculations(Post)

      assert %Calculation{name: :another_cal_but_private} =
               Ash.Resource.Info.calculation(Post, :another_cal_but_private)

      assert nil == Ash.Resource.Info.public_calculation(Post, :another_cal_but_private)

      assert nil == Ash.Resource.Info.calculation(Post, :totally_legit_calculation)
    end

    test "Calculation descriptions are allowed" do
      defposts do
        calculations do
          calculate :name_and_contents, :string, concat([:name, :context]),
            public?: true,
            description: "require one of name/contents"
        end
      end

      assert [
               %Ash.Resource.Calculation{description: "require one of name/contents"}
             ] = Ash.Resource.Info.calculations(Post)
    end
  end

  describe "relationships" do
    test "calculations can access attributes of parent" do
      defmodule Post1 do
        @moduledoc false
        use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets

        attributes do
          uuid_primary_key :id

          attribute :name, :string do
            public?(true)
          end

          attribute :contents, :string do
            public?(true)
          end
        end

        actions do
          default_accept :*
          defaults [:read, :destroy, update: :*, create: :*]
        end
      end

      defmodule PostName1 do
        @moduledoc """
        Calculates the name of the post, from the child comment.
        """
        use Ash.Resource.Calculation

        @impl true
        def load(_query, _opts, _context), do: [:post]

        @impl true
        def strict_loads?, do: false

        @impl true
        def calculate(records, _opts, _) do
          Enum.map(records, fn comment ->
            post = comment.post
            post.name
          end)
        end
      end

      defmodule Comment1 do
        @moduledoc false
        use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets

        attributes do
          uuid_primary_key :id

          attribute :post_id, :uuid do
            allow_nil?(false)
            public? true
          end
        end

        actions do
          default_accept :*
          defaults [:read, :destroy, update: :*, create: :*]
        end

        relationships do
          belongs_to :post, Post1 do
            public?(true)
          end
        end

        calculations do
          calculate :post_name, :string, PostName1 do
            public?(true)
          end
        end
      end

      post =
        Post1
        |> Ash.Changeset.for_create(:create, %{name: "Post 1", contents: "Contents 1"})
        |> Ash.create!()

      comment =
        Comment1
        |> Ash.Changeset.for_create(:create, %{post_id: post.id})
        |> Ash.create!()

      # assert true == true
      comment_with_post_name = Ash.load!(comment, :post_name)
      assert comment_with_post_name.post_name == post.name
    end

    test "calculations can access attributes of parent in multitenant context" do
      defmodule Post2 do
        @moduledoc false
        use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets

        multitenancy do
          strategy(:context)
        end

        attributes do
          uuid_primary_key :id

          attribute :name, :string do
            public?(true)
          end

          attribute :contents, :string do
            public?(true)
          end
        end

        actions do
          default_accept :*
          defaults [:read, :destroy, update: :*, create: :*]
        end
      end

      defmodule PostName2 do
        @moduledoc """
        Calculates the name of the post, from the child comment.
        """
        use Ash.Resource.Calculation

        @impl true
        def load(_query, _opts, _context), do: [:post]

        @impl true
        def strict_loads?, do: false

        @impl true
        def calculate(records, _opts, _) do
          Enum.map(records, fn comment ->
            post = comment.post
            post.name
          end)
        end
      end

      defmodule Comment2 do
        @moduledoc false
        use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets

        multitenancy do
          strategy(:context)
        end

        attributes do
          uuid_primary_key :id

          attribute :post_id, :uuid do
            public?(true)
            allow_nil?(true)
          end
        end

        actions do
          default_accept :*
          defaults [:read, :destroy, update: :*, create: :*]
        end

        relationships do
          belongs_to :post, Post2 do
            public?(true)
          end
        end

        calculations do
          calculate :post_name, :string, PostName2 do
            public?(true)
          end
        end
      end

      tenant_id = "tenant1"

      post =
        Post2
        |> Ash.Changeset.for_create(:create, %{name: "Post 1", contents: "Contents 1"},
          tenant: tenant_id
        )
        |> Ash.create!()

      comment =
        Comment2
        |> Ash.Changeset.for_create(:create, %{post_id: post.id}, tenant: tenant_id)
        |> Ash.create!()

      comment_with_post_name = Ash.load!(comment, :post_name, tenant: tenant_id)
      assert comment_with_post_name.post_name == post.name
    end
  end
end
