defmodule TarakanWeb.RepositoryCommitsLiveTest do
  use TarakanWeb.ConnCase, async: false

  import Phoenix.LiveViewTest

  alias Tarakan.Accounts
  alias Tarakan.HostedRepositories
  alias Tarakan.HostedRepositories.Storage

  @moduletag :tmp_dir

  setup %{tmp_dir: tmp_dir} do
    on_exit(fn ->
      File.rm_rf(Application.fetch_env!(:tarakan, Tarakan.HostedRepositories)[:root])
    end)

    Tarakan.RepositoryCode.Cache.clear()

    account = account_fixture()
    scope = Accounts.scope_for_account(account)
    {:ok, repository} = HostedRepositories.create(scope, %{"name" => "history"})

    %{repository: repository, tmp_dir: tmp_dir}
  end

  defp push_commit(repository, tmp_dir, dir_name, message) do
    work = Path.join(tmp_dir, dir_name)

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

    File.write!(Path.join(work, "README.md"), "# #{message}\n")

    env = [
      {"GIT_AUTHOR_NAME", "t"},
      {"GIT_AUTHOR_EMAIL", "t@example.com"},
      {"GIT_COMMITTER_NAME", "t"},
      {"GIT_COMMITTER_EMAIL", "t@example.com"}
    ]

    {_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

  defp push_merge_commit(repository, tmp_dir, dir_name, branch, message) do
    work = Path.join(tmp_dir, dir_name)

    {_output, 0} =
      System.cmd("git", ["clone", "--quiet", Storage.dir(repository), work],
        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"}
    ]

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

    {_output, 0} = System.cmd("git", ["-C", work, "checkout", "--quiet", "-b", branch], 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", main], 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", message, branch],
        env: env,
        stderr_to_stdout: true
      )

    {_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

  defp push_branch_commit(repository, tmp_dir, dir_name, branch, message) do
    work = Path.join(tmp_dir, dir_name)

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

    File.write!(Path.join(work, "FEATURE.md"), "# #{message}\n")

    env = [
      {"GIT_AUTHOR_NAME", "t"},
      {"GIT_AUTHOR_EMAIL", "t@example.com"},
      {"GIT_COMMITTER_NAME", "t"},
      {"GIT_COMMITTER_EMAIL", "t@example.com"}
    ]

    {_output, 0} = System.cmd("git", ["-C", work, "checkout", "--quiet", "-b", branch], env: env)
    {_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", branch],
        env: env,
        stderr_to_stdout: true
      )

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

  test "lists the default branch history and links each commit to its detail page", %{
    conn: conn,
    repository: repository,
    tmp_dir: tmp_dir
  } do
    first_sha = push_commit(repository, tmp_dir, "work-first", "initial")
    tip = push_commit(repository, tmp_dir, "work-second", "second")

    {:ok, view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/commits")
    render_async(view, 1_000)

    assert has_element?(view, "#repository-commits-tab[aria-current='page']")

    # The active tab is still a link: from a commit detail page it points at the
    # commit list, which is the obvious way back and used to be unclickable.
    assert has_element?(view, "a#repository-commits-tab[aria-current='page'][href]")
    assert has_element?(view, "#commits-list a", "second")
    assert has_element?(view, "#commits-list a", "initial")

    assert has_element?(
             view,
             "#commits-list a[href='/#{repository.owner}/#{repository.name}/commits/#{tip}']"
           )

    assert has_element?(
             view,
             "#commits-list a[href='/#{repository.owner}/#{repository.name}/commits/#{first_sha}']"
           )

    refute has_element?(view, "#commits-load-more")
  end

  test "the commit detail page shows the full message, patch, and code link", %{
    conn: conn,
    repository: repository,
    tmp_dir: tmp_dir
  } do
    _first_sha = push_commit(repository, tmp_dir, "work-first", "initial")
    tip = push_commit(repository, tmp_dir, "work-second", "second")

    {:ok, view, _html} =
      live(conn, "/#{repository.owner}/#{repository.name}/commits/#{tip}")

    render_async(view, 1_000)

    assert has_element?(view, "#commit-subject", "second")
    assert has_element?(view, "#commit-detail-sha", String.slice(tip, 0, 7))
    assert has_element?(view, "#commit-patch", "+# second")

    assert has_element?(
             view,
             "#commit-browse-code[href='/#{repository.owner}/#{repository.name}/code/#{tip}']"
           )

    # The code link opens the browser pinned at this exact commit.
    {:ok, code_view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/code/#{tip}")
    render_async(code_view, 1_000)
    assert has_element?(code_view, "#code-tree")
  end

  test "merge commits render with an empty patch state", %{
    conn: conn,
    repository: repository,
    tmp_dir: tmp_dir
  } do
    _main_sha = push_commit(repository, tmp_dir, "work-first", "initial")
    merge_sha = push_merge_commit(repository, tmp_dir, "work-merge", "side", "merge side")

    {:ok, view, _html} =
      live(conn, "/#{repository.owner}/#{repository.name}/commits/#{merge_sha}")

    render_async(view, 1_000)

    assert has_element?(view, "#commit-subject", "merge side")
    assert has_element?(view, "#commit-patch-empty", "No textual changes")
    refute has_element?(view, "#commits-error")
  end

  test "the commit detail page rejects unreachable commits", %{
    conn: conn,
    repository: repository,
    tmp_dir: tmp_dir
  } do
    _sha = push_commit(repository, tmp_dir, "work-first", "initial")
    unknown = String.duplicate("d", 40)

    {:ok, view, _html} =
      live(conn, "/#{repository.owner}/#{repository.name}/commits/#{unknown}")

    render_async(view, 1_000)

    assert has_element?(view, "#commits-error", "Commit not found")
    refute has_element?(view, "#commit-detail")
  end

  test "the branch picker switches the listed history", %{
    conn: conn,
    repository: repository,
    tmp_dir: tmp_dir
  } do
    _main_sha = push_commit(repository, tmp_dir, "work-first", "initial")

    feature_sha =
      push_branch_commit(repository, tmp_dir, "work-feature", "feature", "feature work")

    {:ok, view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/commits")
    render_async(view, 1_000)

    assert has_element?(view, "#commits-branch-select")
    refute has_element?(view, "#commits-list a", "feature work")

    view
    |> form("#commits-branch-form", %{branch: "feature"})
    |> render_change()

    render_async(view, 1_000)

    assert has_element?(view, "#commits-list a", "feature work")

    assert has_element?(
             view,
             "#commits-list a[href='/#{repository.owner}/#{repository.name}/commits/#{feature_sha}']"
           )
  end

  test "an empty repository shows an empty state", %{conn: conn, repository: repository} do
    {:ok, view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/commits")
    render_async(view, 1_000)

    assert has_element?(view, "#commits-error", "No commits yet")
    refute has_element?(view, "#commits-list")
  end

  test "unknown repositories 404", %{conn: conn} do
    assert_raise Ecto.NoResultsError, fn ->
      live(conn, "/nobody/here/commits")
    end
  end
end
