defmodule Xgit.Repository.Test.RefTest do
  @moduledoc false

  # Not normally part of the public API, but available for implementors of
  # `Xgit.Repository.Storage` behaviour modules. Tests the callbacks related to
  # `Xgit.Ref` to ensure correct implementation of the core contracts.
  # Other tests may be necessary to ensure interop. (For example, the on-disk
  # repository test code adds more tests to ensure correct interop with
  # command-line git.)

  # Users of this module must provide a `setup` callback that provides a
  # `repo` member. This repository may be of any type, but should be "empty."
  # An empty repo has the same data structures as an on-disk repo created
  # via `git init` in a previously-empty directory.

  import Xgit.Util.SharedTestCase

  define_shared_tests do
    alias Xgit.Object
    alias Xgit.Ref
    alias Xgit.Repository.Plumbing
    alias Xgit.Repository.Storage

    @test_content 'test content\n'
    @test_content_id "d670460b4b4aece5915caf5c68d12f560a9fe3e4"

    describe "get_ref/2 (shared)" do
      test "default repo contains HEAD reference", %{repo: repo} do
        assert {:ok, %Xgit.Ref{name: "HEAD", target: "ref: refs/heads/master"}} =
                 Storage.get_ref(repo, "HEAD", follow_link?: false)

        assert {:error, :not_found} = Storage.get_ref(repo, "HEAD", follow_link?: true)
      end

      test "not_found case", %{repo: repo} do
        assert {:error, :not_found} = Storage.get_ref(repo, "refs/heads/master")
      end

      test "invalid_name case", %{repo: repo} do
        assert {:error, :invalid_name} = Storage.get_ref(repo, "refs/../../heads/master")
      end
    end

    describe "list_refs/2 (shared)" do
      test "null case", %{repo: repo} do
        assert {:ok, []} = Storage.list_refs(repo)
      end
    end

    describe "put_ref/3 (shared)" do
      test "error: invalid reference", %{repo: repo} do
        assert {:error, :invalid_ref} =
                 Storage.put_ref(repo, %Ref{
                   name: "refs/heads/master",
                   target: "532ad3cb2518ad13a91e717998a26a6028df062"
                 })
      end

      test "error: object must exist", %{repo: repo} do
        assert {:error, :target_not_found} =
                 Storage.put_ref(repo, %Ref{
                   name: "refs/heads/master",
                   target: "532ad3cb2518ad13a91e717998a26a6028df0623"
                 })
      end

      test "target reference need not exist", %{repo: repo} do
        assert :ok =
                 Storage.put_ref(repo, %Ref{
                   name: "refs/heads/mumble",
                   target: "ref: refs/heads/other"
                 })

        assert {:ok, %Xgit.Ref{name: "refs/heads/mumble", target: "ref: refs/heads/other"}} =
                 Storage.get_ref(repo, "refs/heads/mumble", follow_link?: false)
      end

      test "object exists, but is not a commit", %{repo: repo} do
        object = %Object{type: :blob, content: @test_content, size: 13, id: @test_content_id}
        :ok = Storage.put_loose_object(repo, object)

        assert :ok =
                 Storage.put_ref(repo, %Ref{
                   name: "refs/heads/master",
                   target: @test_content_id
                 })
      end

      test "happy path: results visible to list_refs/1 and get_ref/2", %{repo: repo} do
        {:ok, commit_id_master} =
          Plumbing.hash_object('shhh... not really a commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref = %Ref{
          name: "refs/heads/master",
          target: commit_id_master
        }

        assert :ok = Storage.put_ref(repo, master_ref)

        assert {:ok, [^master_ref]} = Storage.list_refs(repo)

        {:ok, commit_id_other} =
          Plumbing.hash_object('shhh... another fake commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        other_ref = %Ref{
          name: "refs/heads/other",
          target: commit_id_other
        }

        assert :ok = Storage.put_ref(repo, other_ref)

        assert {:ok, ^master_ref} = Storage.get_ref(repo, "refs/heads/master")
        assert {:ok, ^other_ref} = Storage.get_ref(repo, "refs/heads/other")

        assert {:ok, [^master_ref, ^other_ref]} = Storage.list_refs(repo)
      end

      test "follows HEAD reference correctly", %{repo: repo} do
        {:ok, commit_id_master} =
          Plumbing.hash_object('shhh... not really a commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        head_ref = %Ref{
          name: "HEAD",
          target: commit_id_master
        }

        master_ref = %Ref{
          name: "refs/heads/master",
          target: commit_id_master
        }

        master_ref_via_head = %Ref{
          name: "HEAD",
          target: commit_id_master,
          link_target: "refs/heads/master"
        }

        assert :ok = Storage.put_ref(repo, head_ref)

        assert {:ok, [^master_ref]} = Storage.list_refs(repo)
        assert {:ok, ^master_ref} = Storage.get_ref(repo, "refs/heads/master")
        assert {:ok, ^master_ref_via_head} = Storage.get_ref(repo, "HEAD")
      end

      test "can replace an existing object ID ref with a symbolic ref", %{repo: repo} do
        {:ok, commit_id} =
          Plumbing.hash_object('shhh... not really a commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        foo_ref = %Ref{
          name: "refs/heads/foo",
          target: commit_id
        }

        assert :ok = Storage.put_ref(repo, foo_ref)

        assert {:ok, [^foo_ref]} = Storage.list_refs(repo)

        foo_ref2 = %Ref{
          name: "refs/heads/foo",
          target: "ref: refs/heads/master"
        }

        assert :ok = Storage.put_ref(repo, foo_ref2)

        assert {:ok, ^foo_ref2} = Storage.get_ref(repo, "refs/heads/foo", follow_link?: false)
        assert {:ok, [^foo_ref2]} = Storage.list_refs(repo)
      end

      test "can retarget a symbolic ref by using follow_link?: false", %{repo: repo} do
        foo_ref = %Ref{
          name: "refs/heads/foo",
          target: "ref: refs/heads/master"
        }

        assert :ok = Storage.put_ref(repo, foo_ref)

        assert {:ok, [^foo_ref]} = Storage.list_refs(repo)

        foo_ref2 = %Ref{
          name: "refs/heads/foo",
          target: "ref: refs/heads/other"
        }

        assert :ok = Storage.put_ref(repo, foo_ref2, follow_link?: false)

        assert {:ok, ^foo_ref2} = Storage.get_ref(repo, "refs/heads/foo", follow_link?: false)
        assert {:ok, [^foo_ref2]} = Storage.list_refs(repo)
      end

      test ":old_target option (correct match)", %{repo: repo} do
        {:ok, commit_id_master} =
          Plumbing.hash_object('shhh... not really a commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref = %Ref{
          name: "refs/heads/master",
          target: commit_id_master
        }

        assert :ok = Storage.put_ref(repo, master_ref)
        assert {:ok, [^master_ref]} = Storage.list_refs(repo)

        {:ok, commit_id2_master} =
          Plumbing.hash_object('shhh... another not commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref2 = %Ref{
          name: "refs/heads/master",
          target: commit_id2_master
        }

        assert :ok = Storage.put_ref(repo, master_ref2, old_target: commit_id_master)
        assert {:ok, [^master_ref2]} = Storage.list_refs(repo)
      end

      test ":old_target (incorrect match)", %{repo: repo} do
        {:ok, commit_id_master} =
          Plumbing.hash_object('shhh... not really a commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref = %Ref{
          name: "refs/heads/master",
          target: commit_id_master
        }

        assert :ok = Storage.put_ref(repo, master_ref)
        assert {:ok, [^master_ref]} = Storage.list_refs(repo)

        {:ok, commit_id2_master} =
          Plumbing.hash_object('shhh... another not commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref2 = %Ref{
          name: "refs/heads/master",
          target: commit_id2_master
        }

        assert {:error, :old_target_not_matched} =
                 Storage.put_ref(repo, master_ref2,
                   old_target: "2075df9dff2b5a10ad417586b4edde66af849bad"
                 )

        assert {:ok, [^master_ref]} = Storage.list_refs(repo)
      end

      test "put_ref: :old_target (does not exist)", %{repo: repo} do
        {:ok, commit_id_master} =
          Plumbing.hash_object('shhh... not really a commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref = %Ref{
          name: "refs/heads/master",
          target: commit_id_master
        }

        assert :ok = Storage.put_ref(repo, master_ref)
        assert {:ok, [^master_ref]} = Storage.list_refs(repo)

        {:ok, commit_id2_master} =
          Plumbing.hash_object('shhh... another not commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref2 = %Ref{
          name: "refs/heads/master2",
          target: commit_id2_master
        }

        assert {:error, :old_target_not_matched} =
                 Storage.put_ref(repo, master_ref2, old_target: commit_id_master)

        assert {:ok, [^master_ref]} = Storage.list_refs(repo)
      end

      test ":old_target = :new", %{repo: repo} do
        {:ok, commit_id_master} =
          Plumbing.hash_object('shhh... not really a commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref = %Ref{
          name: "refs/heads/master",
          target: commit_id_master
        }

        assert :ok = Storage.put_ref(repo, master_ref, old_target: :new)
        assert {:ok, [^master_ref]} = Storage.list_refs(repo)
      end

      test ":old_target = :new, but target does exist", %{repo: repo} do
        {:ok, commit_id_master} =
          Plumbing.hash_object('shhh... not really a commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref = %Ref{
          name: "refs/heads/master",
          target: commit_id_master
        }

        assert :ok = Storage.put_ref(repo, master_ref)
        assert {:ok, [^master_ref]} = Storage.list_refs(repo)

        {:ok, commit_id2_master} =
          Plumbing.hash_object('shhh... another not commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref2 = %Ref{
          name: "refs/heads/master",
          target: commit_id2_master
        }

        assert {:error, :old_target_not_matched} =
                 Storage.put_ref(repo, master_ref2, old_target: :new)

        assert {:ok, [^master_ref]} = Storage.list_refs(repo)
      end
    end

    describe "delete_ref/3 (shared)" do
      test "removes an existing ref", %{repo: repo} do
        {:ok, commit_id_master} =
          Plumbing.hash_object('shhh... not really a commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref = %Ref{
          name: "refs/heads/master",
          target: commit_id_master
        }

        assert :ok = Storage.put_ref(repo, master_ref)

        assert {:ok, [^master_ref]} = Storage.list_refs(repo)

        assert :ok = Storage.delete_ref(repo, "refs/heads/master")

        assert {:error, :not_found} = Storage.get_ref(repo, "refs/heads/master")
        assert {:ok, []} = Storage.list_refs(repo)
      end

      test "removes symbolic ref but not underlying ref", %{repo: repo} do
        {:ok, commit_id_master} =
          Plumbing.hash_object('shhh... not really a commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref = %Ref{
          name: "refs/heads/master",
          target: commit_id_master
        }

        assert :ok = Storage.put_ref(repo, master_ref)

        other_ref = %Ref{
          name: "refs/heads/other",
          target: "ref: refs/heads/master"
        }

        assert :ok = Storage.put_ref(repo, other_ref)

        assert :ok = Storage.delete_ref(repo, "refs/heads/other", follow_link?: false)

        assert {:error, :not_found} = Storage.get_ref(repo, "refs/heads/other")
        assert {:ok, [^master_ref]} = Storage.list_refs(repo)
      end

      test "quietly 'succeeds' if ref didn't exist", %{repo: repo} do
        assert {:ok, []} = Storage.list_refs(repo)

        assert :ok = Storage.delete_ref(repo, "refs/heads/master")

        assert {:error, :not_found} = Storage.get_ref(repo, "refs/heads/master")
        assert {:ok, []} = Storage.list_refs(repo)
      end

      test "error if name invalid", %{repo: repo} do
        assert {:ok, []} = Storage.list_refs(repo)

        assert {:error, :invalid_ref} = Storage.delete_ref(repo, "refs")

        assert {:error, :not_found} = Storage.get_ref(repo, "refs/heads/master")
        assert {:ok, []} = Storage.list_refs(repo)
      end

      test ":old_target matches existing ref", %{repo: repo} do
        {:ok, commit_id_master} =
          Plumbing.hash_object('shhh... not really a commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref = %Ref{
          name: "refs/heads/master",
          target: commit_id_master
        }

        assert :ok = Storage.put_ref(repo, master_ref)

        assert {:ok, [^master_ref]} = Storage.list_refs(repo)

        assert :ok = Storage.delete_ref(repo, "refs/heads/master", old_target: commit_id_master)

        assert {:error, :not_found} = Storage.get_ref(repo, "refs/heads/master")
        assert {:ok, []} = Storage.list_refs(repo)
      end

      test "doesn't remove ref if :old_target doesn't match", %{repo: repo} do
        {:ok, commit_id_master} =
          Plumbing.hash_object('shhh... not really a commit',
            repo: repo,
            type: :commit,
            validate?: false,
            write?: true
          )

        master_ref = %Ref{
          name: "refs/heads/master",
          target: commit_id_master
        }

        assert :ok = Storage.put_ref(repo, master_ref)

        assert {:ok, [^master_ref]} = Storage.list_refs(repo)

        assert {:error, :old_target_not_matched} =
                 Storage.delete_ref(repo, "refs/heads/master",
                   old_target: "bec43c416143e6b8bf9a3b559260185757e1386b"
                 )

        assert {:ok, ^master_ref} = Storage.get_ref(repo, "refs/heads/master")
        assert {:ok, [^master_ref]} = Storage.list_refs(repo)
      end

      test "error if :old_target specified and no ref exists", %{repo: repo} do
        assert {:ok, []} = Storage.list_refs(repo)

        assert {:error, :old_target_not_matched} =
                 Storage.delete_ref(repo, "refs/heads/master",
                   old_target: "bec43c416143e6b8bf9a3b559260185757e1386b"
                 )

        assert {:error, :not_found} = Storage.get_ref(repo, "refs/heads/master")
        assert {:ok, []} = Storage.list_refs(repo)
      end
    end
  end
end
