defmodule Tarakan.Git.LocalTest do
  use ExUnit.Case, async: true

  alias Tarakan.Git.Local

  @moduletag :tmp_dir

  setup %{tmp_dir: tmp_dir} do
    bare = Path.join(tmp_dir, "fixture.git")
    work = Path.join(tmp_dir, "work")

    {_output, 0} = System.cmd("git", ["init", "--bare", "--quiet", bare])
    {_output, 0} = System.cmd("git", ["clone", "--quiet", bare, work], stderr_to_stdout: true)

    File.write!(Path.join(work, "README.md"), "# Fixture\n")
    File.mkdir_p!(Path.join(work, "lib"))
    File.write!(Path.join(work, "lib/app.ex"), "defmodule App do\nend\n")

    git_in_work = fn args ->
      {_output, 0} =
        System.cmd("git", ["-C", work | args],
          stderr_to_stdout: true,
          env: [
            {"GIT_AUTHOR_NAME", "t"},
            {"GIT_AUTHOR_EMAIL", "t@example.com"},
            {"GIT_COMMITTER_NAME", "t"},
            {"GIT_COMMITTER_EMAIL", "t@example.com"}
          ]
        )
    end

    git_in_work.(["add", "."])
    git_in_work.(["commit", "--quiet", "-m", "initial"])
    git_in_work.(["push", "--quiet", "origin", "HEAD"])

    {sha, 0} = System.cmd("git", ["-C", work, "rev-parse", "HEAD"])
    %{bare: bare, work: work, sha: String.trim(sha)}
  end

  defp push_commit(work, filename, content, message) do
    env = [
      {"GIT_AUTHOR_NAME", "t"},
      {"GIT_AUTHOR_EMAIL", "t@example.com"},
      {"GIT_COMMITTER_NAME", "t"},
      {"GIT_COMMITTER_EMAIL", "t@example.com"}
    ]

    File.write!(Path.join(work, filename), content)
    {_output, 0} = System.cmd("git", ["-C", work, "add", "."], env: env)

    {_output, 0} =
      System.cmd("git", ["-C", work, "commit", "--quiet", "-m", message], env: env)

    {_output, 0} =
      System.cmd("git", ["-C", work, "push", "--quiet", "origin", "HEAD"],
        env: env,
        stderr_to_stdout: true
      )

    {sha, 0} = System.cmd("git", ["-C", work, "rev-parse", "HEAD"])
    String.trim(sha)
  end

  test "reads a commit, tree, and blob from a bare repository", %{bare: bare, sha: sha} do
    assert Local.has_commit?(bare, sha)

    assert {:ok, commit} = Local.read_commit(bare, sha)
    assert commit.sha == sha
    assert %DateTime{} = commit.committed_at

    assert {:ok, tree} = Local.read_tree(bare, commit.tree_sha, false)
    refute tree.truncated
    paths = Enum.map(tree.entries, & &1.path)
    assert "README.md" in paths
    assert "lib" in paths

    readme = Enum.find(tree.entries, &(&1.path == "README.md"))
    assert readme.type == "blob"
    assert {:ok, blob} = Local.read_blob(bare, readme.sha)
    assert blob.content == "# Fixture\n"
    assert blob.size == byte_size("# Fixture\n")
  end

  test "recursive tree includes nested entries", %{bare: bare, sha: sha} do
    {:ok, commit} = Local.read_commit(bare, sha)
    assert {:ok, tree} = Local.read_tree(bare, commit.tree_sha, true)
    assert Enum.any?(tree.entries, &(&1.path == "lib/app.ex"))
  end

  test "resolves HEAD to branch and sha", %{bare: bare, sha: sha} do
    assert {:ok, %{branch: branch, sha: ^sha}} = Local.head_commit(bare)
    assert branch in ["main", "master"]
    assert {:ok, branches} = Local.branches(bare)
    assert branch in branches
  end

  test "unborn HEAD reports empty", %{tmp_dir: tmp_dir} do
    empty = Path.join(tmp_dir, "empty.git")
    {_output, 0} = System.cmd("git", ["init", "--bare", "--quiet", empty])
    assert Local.head_commit(empty) == :empty
  end

  test "misses on unknown objects and missing directories", %{bare: bare, tmp_dir: tmp_dir} do
    unknown = String.duplicate("a", 40)
    assert Local.read_commit(bare, unknown) == :miss
    assert Local.read_blob(bare, unknown) == :miss
    assert Local.read_commit(Path.join(tmp_dir, "nope.git"), unknown) == :miss
    refute Local.has_commit?(bare, "not-a-sha")
  end

  test "read_blob enforces the byte cap", %{bare: bare, sha: sha} do
    {:ok, commit} = Local.read_commit(bare, sha)
    {:ok, tree} = Local.read_tree(bare, commit.tree_sha, false)
    readme = Enum.find(tree.entries, &(&1.path == "README.md"))
    assert Local.read_blob(bare, readme.sha, 3) == :miss
  end

  test "log walks history newest first with author and subject", %{
    bare: bare,
    work: work,
    sha: first_sha
  } do
    tip = push_commit(work, "CHANGELOG.md", "# Changes\n", "add changelog")

    assert {:ok, [newest, oldest]} = Local.log(bare, tip, 50)
    assert newest.sha == tip
    assert newest.subject == "add changelog"
    assert newest.author_name == "t"
    assert newest.author_email == "t@example.com"
    assert %DateTime{} = newest.committed_at
    assert oldest.sha == first_sha
    assert oldest.subject == "initial"

    assert {:ok, [only]} = Local.log(bare, tip, 1)
    assert only.sha == tip

    assert Local.log(bare, String.duplicate("a", 40), 50) == :miss
  end

  test "ancestor? checks reachability from a tip", %{bare: bare, work: work, sha: first_sha} do
    tip = push_commit(work, "CHANGELOG.md", "# Changes\n", "add changelog")

    assert Local.ancestor?(bare, first_sha, tip)
    assert Local.ancestor?(bare, tip, tip)
    refute Local.ancestor?(bare, tip, first_sha)
    refute Local.ancestor?(bare, String.duplicate("a", 40), tip)
  end

  test "show_commit reads the full message and patch", %{
    bare: bare,
    work: work,
    sha: first_sha
  } do
    tip = push_commit(work, "CHANGELOG.md", "# Changes\n", "add changelog\n\nwith a body")

    assert {:ok, shown} = Local.show_commit(bare, tip)
    assert shown.sha == tip
    assert shown.message == "add changelog\n\nwith a body"
    assert shown.author_name == "t"
    assert %DateTime{} = shown.committed_at
    assert shown.patch =~ "diff --git a/CHANGELOG.md b/CHANGELOG.md"
    assert shown.patch =~ "+# Changes"
    refute shown.patch_truncated

    # The root commit diffs against the empty tree.
    assert {:ok, root} = Local.show_commit(bare, first_sha)
    assert root.patch =~ "diff --git a/README.md b/README.md"

    assert Local.show_commit(bare, String.duplicate("a", 40)) == :miss
  end

  test "show_commit caps oversized patches", %{bare: bare, work: work} do
    tip = push_commit(work, "big.txt", String.duplicate("x\n", 500), "big")

    assert {:ok, shown} = Local.show_commit(bare, tip, 200)
    assert shown.patch_truncated
    assert byte_size(shown.patch) <= 200
    assert String.valid?(shown.patch)
  end

  test "show_commit reads merge commits with an empty patch", %{bare: bare, work: work} do
    env = [
      {"GIT_AUTHOR_NAME", "t"},
      {"GIT_AUTHOR_EMAIL", "t@example.com"},
      {"GIT_COMMITTER_NAME", "t"},
      {"GIT_COMMITTER_EMAIL", "t@example.com"}
    ]

    {branch, 0} = System.cmd("git", ["-C", work, "rev-parse", "--abbrev-ref", "HEAD"])
    branch = String.trim(branch)

    {_output, 0} = System.cmd("git", ["-C", work, "checkout", "--quiet", "-b", "side"], env: env)
    File.write!(Path.join(work, "side.txt"), "side\n")
    {_output, 0} = System.cmd("git", ["-C", work, "add", "."], env: env)

    {_output, 0} =
      System.cmd("git", ["-C", work, "commit", "--quiet", "-m", "side change"], env: env)

    {_output, 0} = System.cmd("git", ["-C", work, "checkout", "--quiet", branch], env: env)
    File.write!(Path.join(work, "main.txt"), "main\n")
    {_output, 0} = System.cmd("git", ["-C", work, "add", "."], env: env)

    {_output, 0} =
      System.cmd("git", ["-C", work, "commit", "--quiet", "-m", "main change"], env: env)

    {_output, 0} =
      System.cmd("git", ["-C", work, "merge", "--quiet", "--no-ff", "-m", "merge side", "side"],
        env: env,
        stderr_to_stdout: true
      )

    {_output, 0} =
      System.cmd("git", ["-C", work, "push", "--quiet", "origin", "HEAD"],
        env: env,
        stderr_to_stdout: true
      )

    {merge_sha, 0} = System.cmd("git", ["-C", work, "rev-parse", "HEAD"])
    merge_sha = String.trim(merge_sha)

    assert {:ok, shown} = Local.show_commit(bare, merge_sha)
    assert shown.sha == merge_sha
    assert shown.message == "merge side"
    assert shown.patch == ""
    refute shown.patch_truncated
  end
end
