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

  alias Ash.Resource.Relationships.HasMany
  alias Ash.Resource.Relationships.ManyToMany
  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, data_layer: Ash.DataLayer.Ets

        attributes do
          uuid_primary_key :id
        end

        unquote(body)
      end

      alias unquote(module), as: Post
    end
  end

  describe "representation" do
    test "it creates a relationship and a join relationship" do
      defposts do
        relationships do
          many_to_many :related_posts, __MODULE__,
            through: SomeResource,
            source_attribute_on_join_resource: :post_id,
            destination_attribute_on_join_resource: :related_post_id,
            public?: true,
            default_sort: [inserted_at: :desc]

          many_to_many :unrelated_posts, __MODULE__,
            through: Tabloid,
            source_attribute_on_join_resource: :post_id,
            destination_attribute_on_join_resource: :unrelated_post_id
        end
      end

      assert [
               %HasMany{
                 cardinality: :many,
                 destination: Tabloid,
                 destination_attribute: :post_id,
                 name: :unrelated_posts_join_assoc,
                 source: Post,
                 source_attribute: :id,
                 type: :has_many,
                 public?: false
               },
               %HasMany{
                 cardinality: :many,
                 destination: SomeResource,
                 destination_attribute: :post_id,
                 name: :related_posts_join_assoc,
                 source: Post,
                 source_attribute: :id,
                 type: :has_many,
                 public?: false
               },
               %ManyToMany{
                 cardinality: :many,
                 destination: Post,
                 destination_attribute: :id,
                 destination_attribute_on_join_resource: :related_post_id,
                 name: :related_posts,
                 source: Post,
                 source_attribute: :id,
                 source_attribute_on_join_resource: :post_id,
                 through: SomeResource,
                 type: :many_to_many,
                 public?: true,
                 default_sort: [inserted_at: :desc]
               },
               %ManyToMany{
                 cardinality: :many,
                 destination: Post,
                 destination_attribute: :id,
                 destination_attribute_on_join_resource: :unrelated_post_id,
                 name: :unrelated_posts,
                 source: Post,
                 source_attribute: :id,
                 source_attribute_on_join_resource: :post_id,
                 through: Tabloid,
                 type: :many_to_many,
                 public?: false
               }
             ] = Ash.Resource.Info.relationships(Post)

      assert [%ManyToMany{name: :related_posts}] = Ash.Resource.Info.public_relationships(Post)

      assert %ManyToMany{name: :related_posts} =
               Ash.Resource.Info.public_relationship(Post, :related_posts)

      assert nil == Ash.Resource.Info.relationship(Post, :definitely_legit_relationship)

      assert nil == Ash.Resource.Info.public_relationship(Post, :unrelated_posts)
    end
  end

  describe "validation" do
    test "it fails if you pass a string to `through`" do
      assert_raise(
        Spark.Error.DslError,
        ~r/expected module in :through option, got: "some_table"/,
        fn ->
          defposts do
            relationships do
              many_to_many :foobars, Foobar,
                through: "some_table",
                source_attribute_on_join_resource: :source_post_id,
                destination_attribute_on_join_resource: :destination_post_id,
                public?: true
            end
          end
        end
      )
    end

    test "you can pass a module to `through`" do
      defposts do
        relationships do
          many_to_many :foobars, Foobar,
            through: FooBars,
            source_attribute_on_join_resource: :source_post_id,
            destination_attribute_on_join_resource: :destination_post_id,
            public?: true
        end
      end
    end

    test "it guesses `source_attribute_on_join_resource` if not set" do
      defposts do
        relationships do
          many_to_many :authors, Author do
            through PostsJoinArticlesAuthors
            destination_attribute_on_join_resource :manager_id
            public?(true)
          end
        end
      end

      assert [
               %HasMany{},
               %ManyToMany{
                 destination: Author,
                 destination_attribute_on_join_resource: :manager_id,
                 source_attribute_on_join_resource: :post_id,
                 through: PostsJoinArticlesAuthors
               }
             ] = Ash.Resource.Info.relationships(Post)
    end

    test "it guesses `destination_attribute_on_join_resource` if not set" do
      defposts do
        relationships do
          many_to_many :authors, ArticleAuthor do
            through PostsJoinArticlesAuthors
            source_attribute_on_join_resource :article_id
            public?(true)
          end
        end
      end

      assert [
               %HasMany{},
               %ManyToMany{
                 destination: ArticleAuthor,
                 destination_attribute_on_join_resource: :article_author_id,
                 source_attribute_on_join_resource: :article_id,
                 through: PostsJoinArticlesAuthors
               }
             ] = Ash.Resource.Info.relationships(Post)
    end

    test "it fails if you dont pass an atom for `source_attribute_on_join_resource`" do
      assert_raise(
        Spark.Error.DslError,
        ~r/invalid value for :source_attribute_on_join_resource option: expected atom, got: "what"/,
        fn ->
          defposts do
            relationships do
              many_to_many :foobars, Foobar,
                through: FooBars,
                source_attribute_on_join_resource: "what",
                destination_attribute_on_join_resource: :destination_post_id,
                public?: true
            end
          end
        end
      )
    end

    test "it fails if you dont pass an atom for `destination_attribute_on_join_resource`" do
      assert_raise(
        Spark.Error.DslError,
        ~r/invalid value for :destination_attribute_on_join_resource option: expected atom, got: "what"/,
        fn ->
          defposts do
            relationships do
              many_to_many :foobars, Foobar,
                through: FooBar,
                destination_attribute_on_join_resource: "what",
                source_attribute_on_join_resource: :source_post_id,
                public?: true
            end
          end
        end
      )
    end

    test "it fails if you dont pass an atom for `source_attribute`" do
      assert_raise(
        Spark.Error.DslError,
        ~r/invalid value for :source_attribute option: expected atom, got: "what"/,
        fn ->
          defposts do
            relationships do
              many_to_many :foobars, Foobar,
                through: FooBar,
                source_attribute: "what",
                source_attribute_on_join_resource: :source_post_id,
                destination_attribute_on_join_resource: :destination_post_id,
                public?: true
            end
          end
        end
      )
    end

    test "it fails if you dont pass an atom for `destination_attribute`" do
      assert_raise(
        Spark.Error.DslError,
        ~r/invalid value for :destination_attribute option: expected atom, got: "what"/,
        fn ->
          defposts do
            relationships do
              many_to_many :foobars, Foobar,
                through: FooBars,
                destination_attribute: "what",
                source_attribute_on_join_resource: :source_post_id,
                destination_attribute_on_join_resource: :destination_post_id,
                public?: true
            end
          end
        end
      )
    end

    test "fails if public? is not an boolean" do
      assert_raise(
        Spark.Error.DslError,
        ~r/invalid value for :public\? option: expected boolean, got: "an_invalid_field"/,
        fn ->
          defposts do
            relationships do
              many_to_many :foobars, Foobar,
                through: FooBars,
                source_attribute_on_join_resource: :source_post_id,
                destination_attribute_on_join_resource: :destination_post_id,
                public?: "an_invalid_field"
            end
          end
        end
      )
    end
  end
end
