# frozen_string_literal: true

require "cases/helper"

class ActiveRecord::Relation
  class WhereClauseTest < ActiveRecord::TestCase
    test "+ combines two where clauses" do
      first_clause = WhereClause.new([table["id"].eq(bind_param(1))])
      second_clause = WhereClause.new([table["name"].eq(bind_param("Sean"))])
      combined = WhereClause.new(
        [table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))],
      )

      assert_equal combined, first_clause + second_clause
    end

    test "+ is associative, but not commutative" do
      a = WhereClause.new(["a"])
      b = WhereClause.new(["b"])
      c = WhereClause.new(["c"])

      assert_equal a + (b + c), (a + b) + c
      assert_not_equal a + b, b + a
    end

    test "an empty where clause is the identity value for +" do
      clause = WhereClause.new([table["id"].eq(bind_param(1))])

      assert_equal clause, clause + WhereClause.empty
    end

    test "merge combines two where clauses" do
      a = WhereClause.new([table["id"].eq(1)])
      b = WhereClause.new([table["name"].eq("Sean")])
      expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")])

      assert_equal expected, a.merge(b)
    end

    test "merge keeps the right side, when two equality clauses reference the same column" do
      a = WhereClause.new([table["id"].eq(1), table["name"].eq("Sean")])
      b = WhereClause.new([table["name"].eq("Jim")])
      expected = WhereClause.new([table["id"].eq(1), table["name"].eq("Jim")])

      assert_equal expected, a.merge(b)
    end

    test "merge removes bind parameters matching overlapping equality clauses" do
      a = WhereClause.new(
        [table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))],
      )
      b = WhereClause.new(
        [table["name"].eq(bind_param("Jim"))],
      )
      expected = WhereClause.new(
        [table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Jim"))],
      )

      assert_equal expected, a.merge(b)
    end

    test "merge allows for columns with the same name from different tables" do
      table2 = Arel::Table.new("table2")
      a = WhereClause.new(
        [table["id"].eq(bind_param(1)), table2["id"].eq(bind_param(2))],
      )
      b = WhereClause.new(
        [table["id"].eq(bind_param(3))],
      )
      expected = WhereClause.new(
        [table2["id"].eq(bind_param(2)), table["id"].eq(bind_param(3))],
      )

      assert_equal expected, a.merge(b)
    end

    test "merge with or/and correctly handles SQL literals and attributes" do
      a = WhereClause.new([
        table["id"].eq(1),
        Arel::Nodes::Grouping.new(Arel.sql("foo = bar"))
      ])
      b = (a + WhereClause.new([table["attr_1"].eq("val_1")])).or(
        WhereClause.new([table["attr_2"].eq("val_2")])
      )

      expected = WhereClause.new([
        table["id"].eq(1),
        Arel::Nodes::Grouping.new(Arel.sql("foo = bar")),
        Arel::Nodes::Grouping.new(
          Arel::Nodes::Or.new([
            Arel::Nodes::And.new([
              table["id"].eq(1),
              Arel::Nodes::Grouping.new(Arel.sql("foo = bar")),
              table["attr_1"].eq("val_1")
            ]),
            table["attr_2"].eq("val_2")
          ])
        )
      ])

      assert_equal expected, a.merge(b)
    end

    test "a clause knows if it is empty" do
      assert_empty WhereClause.empty
      assert_not_empty WhereClause.new(["anything"])
    end

    test "invert cannot handle nil" do
      where_clause = WhereClause.new([nil])

      assert_raises ArgumentError do
        where_clause.invert
      end
    end

    test "invert wraps the ast inside a NAND node" do
      original = WhereClause.new([
        table["id"].in([1, 2, 3]),
        table["id"].not_in([1, 2, 3]),
        table["id"].eq(1),
        table["id"].not_eq(2),
        table["id"].gt(1),
        table["id"].gteq(2),
        table["id"].lt(1),
        table["id"].lteq(2),
        table["id"].is_not_distinct_from(1),
        table["id"].is_distinct_from(2),
        "sql literal"
      ])
      expected = WhereClause.new([
        Arel::Nodes::Not.new(
          Arel::Nodes::And.new([
            table["id"].in([1, 2, 3]),
            table["id"].not_in([1, 2, 3]),
            table["id"].eq(1),
            table["id"].not_eq(2),
            table["id"].gt(1),
            table["id"].gteq(2),
            table["id"].lt(1),
            table["id"].lteq(2),
            table["id"].is_not_distinct_from(1),
            table["id"].is_distinct_from(2),
            Arel::Nodes::Grouping.new("sql literal")
          ])
        )
      ])

      assert_equal expected, original.invert
    end

    test "except removes binary predicates referencing a given column" do
      where_clause = WhereClause.new([
        table["id"].in([1, 2, 3]),
        table["name"].eq(bind_param("Sean")),
        table["age"].gteq(bind_param(30)),
      ])
      expected = WhereClause.new([table["age"].gteq(bind_param(30))])

      assert_equal expected, where_clause.except("id", "name")
    end

    test "except jumps over unhandled binds (like with OR) correctly" do
      wcs = (0..9).map do |i|
        WhereClause.new([table["id#{i}"].eq(bind_param(i))])
      end

      wc = wcs[0] + wcs[1] + wcs[2].or(wcs[3]) + wcs[4] + wcs[5] + wcs[6].or(wcs[7]) + wcs[8] + wcs[9]

      expected = wcs[0] + wcs[2].or(wcs[3]) + wcs[5] + wcs[6].or(wcs[7]) + wcs[9]
      actual = wc.except("id1", "id2", "id4", "id7", "id8")

      assert_equal expected, actual
    end

    test "ast groups its predicates with AND" do
      predicates = [
        table["id"].in([1, 2, 3]),
        table["name"].eq(bind_param(nil)),
      ]
      where_clause = WhereClause.new(predicates)
      expected = Arel::Nodes::And.new(predicates)

      assert_equal expected, where_clause.ast
    end

    test "ast wraps any SQL literals in parenthesis" do
      random_object = Object.new
      where_clause = WhereClause.new([
        table["id"].in([1, 2, 3]),
        "foo = bar",
        random_object,
      ])
      expected = Arel::Nodes::And.new([
        table["id"].in([1, 2, 3]),
        Arel::Nodes::Grouping.new(Arel.sql("foo = bar")),
        random_object,
      ])

      assert_equal expected, where_clause.ast
    end

    test "ast removes any empty strings" do
      where_clause = WhereClause.new([table["id"].in([1, 2, 3])])
      where_clause_with_empty = WhereClause.new([table["id"].in([1, 2, 3]), ""])

      assert_equal where_clause.ast, where_clause_with_empty.ast
    end

    test "or joins the two clauses using OR" do
      where_clause = WhereClause.new([table["id"].eq(bind_param(1))])
      other_clause = WhereClause.new([table["name"].eq(bind_param("Sean"))])
      expected_ast =
        Arel::Nodes::Grouping.new(
          Arel::Nodes::Or.new([table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))])
        )

      assert_equal expected_ast.to_sql, where_clause.or(other_clause).ast.to_sql
    end

    test "or returns an empty where clause when either side is empty" do
      where_clause = WhereClause.new([table["id"].eq(bind_param(1))])

      assert_equal WhereClause.empty, where_clause.or(WhereClause.empty)
      assert_equal WhereClause.empty, WhereClause.empty.or(where_clause)
    end

    test "or places common conditions before the OR" do
      a = WhereClause.new(
        [table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))],
      )
      b = WhereClause.new(
        [table["id"].eq(bind_param(1)), table["hair_color"].eq(bind_param("black"))],
      )

      common = WhereClause.new(
        [table["id"].eq(bind_param(1))],
      )

      or_clause = WhereClause.new([table["name"].eq(bind_param("Sean"))])
        .or(WhereClause.new([table["hair_color"].eq(bind_param("black"))]))

      assert_equal common + or_clause, a.or(b)
    end

    test "or can detect identical or as being a common condition" do
      common_or = WhereClause.new([table["name"].eq(bind_param("Sean"))])
        .or(WhereClause.new([table["hair_color"].eq(bind_param("black"))]))

      a = common_or + WhereClause.new([table["id"].eq(bind_param(1))])
      b = common_or + WhereClause.new([table["foo"].eq(bind_param("bar"))])

      new_or = WhereClause.new([table["id"].eq(bind_param(1))])
        .or(WhereClause.new([table["foo"].eq(bind_param("bar"))]))

      assert_equal common_or + new_or, a.or(b)
    end

    test "or will use only common conditions if one side only has common conditions" do
      only_common = WhereClause.new([
        table["id"].eq(bind_param(1)),
        "foo = bar",
      ])

      common_with_extra = WhereClause.new([
        table["id"].eq(bind_param(1)),
        "foo = bar",
        table["extra"].eq(bind_param("pluto")),
      ])

      assert_equal only_common, only_common.or(common_with_extra)
      assert_equal only_common, common_with_extra.or(only_common)
    end

    test "supports hash equality" do
      h = Hash.new(0)
      h[WhereClause.new(["a"])] += 1
      h[WhereClause.new(["a"])] += 1
      h[WhereClause.new(["b"])] += 1

      expected = {
        WhereClause.new(["a"]) => 2,
        WhereClause.new(["b"]) => 1
      }
      assert_equal expected, h
    end

    private
      def table
        Arel::Table.new("table")
      end

      def bind_param(value)
        Arel::Nodes::BindParam.new(value)
      end
  end
end
