defmodule Vax.AdapterIntegrationTest do
  use ExUnit.Case, async: true
  @moduletag :integration

  defmodule TestRepo do
    use Ecto.Repo, adapter: Vax.Adapter, otp_app: :vax
  end

  defmodule MySchema do
    use Vax.Schema

    schema "my_schema" do
      field(:name, :string)
      field(:age, :integer)
      field(:number_of_friends, :integer, default: 2)
      field(:number_of_followers, :integer)
      field(:likes_given, Vax.Types.Counter, default: 0)
      field(:favorite_numbers, Vax.Types.Set, type: :integer)
      field(:public_profile?, Vax.Types.Flag, strategy: :disable_wins)
      field(:metadata, Vax.Types.Map)
    end
  end

  setup_all %{} do
    hostname = Application.get_env(:vax, :hostname)
    port = Application.get_env(:vax, :port)

    start_supervised!({TestRepo, hostname: hostname, port: port, log: true})

    :ok
  end

  test "checking out a connection" do
    refute TestRepo.checked_out?()

    TestRepo.checkout(fn ->
      assert TestRepo.checked_out?()
    end)

    refute TestRepo.checked_out?()
  end

  test "performing actions on checked out connection" do
    TestRepo.checkout(fn ->
      id = Ecto.UUID.generate()
      assert 0 = TestRepo.read_counter(id)
      assert :ok = TestRepo.increment_counter(id, 10)
      assert 10 = TestRepo.read_counter(id)
      assert :ok = TestRepo.increment_counter(id, 20)
      assert 30 = TestRepo.read_counter(id)
    end)
  end

  test "reading and increasing a counter" do
    id = Ecto.UUID.generate()
    assert 0 = TestRepo.read_counter(id)
    assert :ok = TestRepo.increment_counter(id, 10)
    assert 10 = TestRepo.read_counter(id)
    assert :ok = TestRepo.increment_counter(id, 20)
    assert 30 = TestRepo.read_counter(id)
  end

  # Step by step:
  # 1. Transaction 2 starts
  # 2. Transaction 1 starts
  # 3. Transaction 1 increments the counter by 10, reads it, asserts its 10
  # 4. Transaction 2 reads the counter, asserts its 0
  # 5. Both transactions commit
  test "Transaction support" do
    id = Ecto.UUID.generate()
    parent = self()

    p1 =
      spawn(fn ->
        assert_receive {:start, p2}

        TestRepo.transaction(fn ->
          assert 0 = TestRepo.read_counter(id)
          assert :ok = TestRepo.increment_counter(id, 10)
          assert 10 = TestRepo.read_counter(id)
          send(p2, :read)
          assert_receive :commit
        end)

        send(parent, :transaction_1_commited)
      end)

    _p2 =
      spawn(fn ->
        TestRepo.transaction(fn ->
          send(p1, {:start, self()})
          assert_receive :read
          assert 0 = TestRepo.read_counter(id)
        end)

        send(parent, :transaction_2_commited)
        send(p1, :commit)
      end)

    assert_receive :transaction_2_commited
    assert_receive :transaction_1_commited
  end

  describe "insert/2" do
    test "inserts raw struct" do
      id = Ecto.UUID.generate()

      assert {:ok, %MySchema{id: ^id}} = TestRepo.insert(%MySchema{id: id, name: "Carl", age: 26})

      assert %MySchema{id: ^id, name: "Carl", age: 26, likes_given: 0} =
               TestRepo.get(MySchema, id)
    end

    test "inserts with a valid changeset" do
      id = Ecto.UUID.generate()
      changeset = Ecto.Changeset.change(%MySchema{id: id, name: "Carl", age: 26, likes_given: 3})

      assert {:ok, %MySchema{id: ^id}} = TestRepo.insert(changeset)

      assert %MySchema{id: ^id, name: "Carl", age: 26, likes_given: 3} =
               TestRepo.get(MySchema, id)
    end

    test "maps" do
      changeset = Ecto.Changeset.change(%MySchema{name: "Carl", metadata: %{"gamba" => true}})

      assert {:ok, %MySchema{metadata: %{"gamba" => true}} = entry} = TestRepo.insert(changeset)

      assert %MySchema{metadata: %{"gamba" => true}} = TestRepo.get(MySchema, entry.id)

      {:ok, %MySchema{metadata: %{"gamba" => true, "foo" => "bar"}}} =
        entry
        |> Ecto.Changeset.change(metadata: Map.put(entry.metadata, "foo", "bar"))
        |> TestRepo.update()
    end

    test "inserts with a valid changeset and autogenerated id" do
      changeset =
        Ecto.Changeset.change(%MySchema{
          name: "Carl",
          age: 26,
          likes_given: 3
        })

      assert {:ok, %MySchema{id: id}} = TestRepo.insert(changeset)
      assert %MySchema{id: ^id} = TestRepo.get(MySchema, id)
    end

    test "fails for invalid changeset" do
      changeset =
        %MySchema{name: "Carl"}
        |> Ecto.Changeset.cast(%{age: -10}, [:age])
        |> Ecto.Changeset.validate_number(:age, greater_than_or_equal_to: 0)

      assert {:error, %Ecto.Changeset{valid?: false}} = TestRepo.insert(changeset)
    end
  end

  describe "insert!/2" do
    test "raises for invalid changeset" do
      assert_raise Ecto.InvalidChangesetError, fn ->
        %MySchema{name: "Carl"}
        |> Ecto.Changeset.cast(%{age: -10}, [:age])
        |> Ecto.Changeset.validate_number(:age, greater_than_or_equal_to: 0)
        |> TestRepo.insert!()
      end
    end
  end

  describe "update/2" do
    test "updating an entry with valid changeset" do
      {:ok, entry} = TestRepo.insert(%MySchema{name: "Carl", age: 26})

      assert {:ok, %{age: 27}} =
               entry
               |> Ecto.Changeset.change(age: 27, likes_given: 2, public_profile?: true)
               |> TestRepo.update()

      assert %MySchema{age: 27, name: "Carl", likes_given: 2, public_profile?: true} =
               TestRepo.get(MySchema, entry.id)
    end

    test "semantically updates CRDT set type with `compute_change`" do
      {:ok, entry} =
        TestRepo.insert(%MySchema{name: "Carl", age: 26, favorite_numbers: MapSet.new([10, 20])})

      # Add 40, 30
      assert {:ok, _} =
               entry
               |> Ecto.Changeset.change(favorite_numbers: MapSet.new([10, 20, 30, 40]))
               |> TestRepo.update()

      # Rm 10
      assert {:ok, _} =
               entry
               |> Ecto.Changeset.change(favorite_numbers: MapSet.new([20]))
               |> TestRepo.update()

      expected_result = MapSet.new([20, 30, 40])
      assert %{favorite_numbers: ^expected_result} = updated_entry = TestRepo.reload(entry)

      # Rm 20, 30, add 50
      assert {:ok, _} =
               updated_entry
               |> Ecto.Changeset.change(favorite_numbers: MapSet.new([40, 50]))
               |> TestRepo.update()

      expected_result = MapSet.new([40, 50])
      assert %{favorite_numbers: ^expected_result} = TestRepo.reload(updated_entry)
    end

    test "semantically updates CRDT counter type with `compute_change`" do
      {:ok, entry} = TestRepo.insert(%MySchema{name: "Carl", age: 26, likes_given: 1})

      # last known value is 1, increasing by 4
      assert {:ok, _} =
               entry
               |> Ecto.Changeset.change(likes_given: 5)
               |> TestRepo.update()

      # last known value is 1, increasing by 9
      assert {:ok, _} =
               entry
               |> Ecto.Changeset.change(likes_given: 10)
               |> TestRepo.update()

      # total incrases is 13
      assert %{likes_given: 14} = updated_entry = TestRepo.reload(entry)

      # last known value is 14, increasing by 2
      assert {:ok, %{likes_given: 16}} =
               updated_entry
               |> Ecto.Changeset.change(likes_given: 16)
               |> TestRepo.update()

      # last known value is 14, decreasing by 4
      assert {:ok, %{likes_given: 12}} =
               updated_entry
               |> Ecto.Changeset.change(likes_given: 10)
               |> TestRepo.update()
    end

    test "fails for invalid changeset" do
      changeset =
        %MySchema{id: Ecto.UUID.generate(), name: "Carl"}
        |> Ecto.Changeset.cast(%{age: -10}, [:age])
        |> Ecto.Changeset.validate_number(:age, greater_than_or_equal_to: 0)

      assert {:error, %Ecto.Changeset{valid?: false}} = TestRepo.update(changeset)
    end
  end

  describe "update!/2" do
    test "raises for invalid changeset" do
      assert_raise Ecto.InvalidChangesetError, fn ->
        %MySchema{id: Ecto.UUID.generate(), name: "Carl"}
        |> Ecto.Changeset.cast(%{age: -10}, [:age])
        |> Ecto.Changeset.validate_number(:age, greater_than_or_equal_to: 0)
        |> TestRepo.update!()
      end
    end
  end

  describe "insert_or_update/2" do
    test "inserts and updates valid changeset" do
      assert {:ok, %{id: id}} =
               %MySchema{name: "Carl"}
               |> Ecto.Changeset.change()
               |> TestRepo.insert_or_update()

      assert %{name: "Carl"} = entry = TestRepo.get(MySchema, id)

      {:ok, %{id: ^id, name: "James"}} =
        entry
        |> Ecto.Changeset.change(name: "James")
        |> TestRepo.insert_or_update()

      assert %{name: "James"} = TestRepo.get(MySchema, id)
    end

    test "fails for invalid changeset" do
      insert_changeset =
        %MySchema{name: "Carl"}
        |> Ecto.Changeset.cast(%{age: -10}, [:age])
        |> Ecto.Changeset.validate_number(:age, greater_than_or_equal_to: 0)

      assert {:error, %Ecto.Changeset{valid?: false}} =
               TestRepo.insert_or_update(insert_changeset)

      {:ok, entry} = TestRepo.insert(%MySchema{name: "John"})

      update_changeset =
        entry
        |> Ecto.Changeset.cast(%{age: -10}, [:age])
        |> Ecto.Changeset.validate_number(:age, greater_than_or_equal_to: 0)

      assert {:error, %Ecto.Changeset{valid?: false}} =
               TestRepo.insert_or_update(update_changeset)
    end
  end

  describe "insert_or_update!/2" do
    test "raises for invalid changeset" do
      assert_raise Ecto.InvalidChangesetError, fn ->
        %MySchema{name: "Carl"}
        |> Ecto.Changeset.cast(%{age: -10}, [:age])
        |> Ecto.Changeset.validate_number(:age, greater_than_or_equal_to: 0)
        |> TestRepo.insert_or_update!()
      end

      {:ok, entry} = TestRepo.insert(%MySchema{name: "John"})

      assert_raise Ecto.InvalidChangesetError, fn ->
        entry
        |> Ecto.Changeset.cast(%{age: -10}, [:age])
        |> Ecto.Changeset.validate_number(:age, greater_than_or_equal_to: 0)
        |> TestRepo.insert_or_update!()
      end
    end
  end

  describe "get" do
    test "returns nil for inexistent keys" do
      assert is_nil(TestRepo.get(MySchema, Ecto.UUID.generate()))
    end
  end

  describe "all" do
    import Ecto.Query

    setup do
      {:ok, entry_a} = TestRepo.insert(%MySchema{name: "John", age: 19})
      {:ok, entry_b} = TestRepo.insert(%MySchema{name: "Alice", age: 22})

      {:ok, entry_a: entry_a, entry_b: entry_b}
    end

    test "can handle `where` with `:==`", %{entry_a: entry_a} do
      assert [] = from(MySchema, where: [id: ^Ecto.UUID.generate()]) |> TestRepo.all()
      assert [%MySchema{} = result] = from(MySchema, where: [id: ^entry_a.id]) |> TestRepo.all()
      assert entry_a.id == result.id
    end

    test "can handle `where` with `in`", %{entry_a: entry_a, entry_b: entry_b} do
      list = [entry_a.id, entry_b.id, Ecto.UUID.generate()]

      assert [result_a, result_b] =
               from(MySchema)
               |> where([m], m.id in ^list)
               |> TestRepo.all()

      assert result_a.id in [entry_a.id, entry_b.id]
      assert result_b.id in [entry_a.id, entry_b.id]
    end

    test "can handle `where` expressions with `or`", %{entry_a: entry_a, entry_b: entry_b} do
      [result_a, result_b] =
        from(MySchema)
        |> where([m], m.id == ^entry_a.id or m.id == ^entry_b.id)
        |> TestRepo.all()

      assert result_a.id in [entry_a.id, entry_b.id]
      assert result_b.id in [entry_a.id, entry_b.id]
    end

    test "raises if trying to use `where` with non pk field" do
      assert_raise(
        RuntimeError,
        "Vax only supports filtering by primary key in where expressions",
        fn ->
          from(MySchema)
          |> where([m], m.age == ^10)
          |> TestRepo.all()
        end
      )
    end

    test "raises if trying to use multiple `where` expressions", %{
      entry_a: entry_a,
      entry_b: entry_b
    } do
      assert_raise(RuntimeError, ~r/Vax only supports a single/, fn ->
        from(MySchema)
        |> where([m], m.id == ^entry_a.id)
        |> where([m], m.id == ^entry_b.id)
        |> TestRepo.all()
      end)
    end

    test "raises if trying to use invalid `where` expression", %{entry_a: entry_a} do
      assert_raise(RuntimeError, ~r/Unsupported expression/, fn ->
        # `and` is one of the unsupported expressions
        from(MySchema)
        |> where([m], m.id == ^entry_a.id and m.id == ^entry_a.id)
        |> TestRepo.all()
      end)
    end
  end

  # FIXME: reset operation seem to be sent correctly, but doesn't take effect
  test "deleting a schema entry" do
    {:ok, %{id: id} = entry} = TestRepo.insert(%MySchema{name: "Carl", age: 25})
    assert {:ok, %MySchema{id: ^id}} = TestRepo.delete(entry)
    assert nil = TestRepo.get(MySchema, id)
  end
end
