defmodule LivebookWeb.AppAuthLiveTest do
  use LivebookWeb.ConnCase, async: true

  import Phoenix.LiveViewTest

  setup ctx do
    {slug, app_pid} = create_app(ctx[:app_settings] || %{})

    on_exit(fn ->
      Livebook.App.close(app_pid)
    end)

    [slug: slug]
  end

  defp create_app(app_settings_attrs) do
    slug = Livebook.Utils.random_long_id()

    app_settings =
      Livebook.Notebook.AppSettings.new()
      |> Map.replace!(:slug, slug)
      |> Map.merge(app_settings_attrs)

    notebook = %{Livebook.Notebook.new() | app_settings: app_settings}
    app_spec = Livebook.Apps.NotebookAppSpec.new(notebook)

    deployer_pid = Livebook.Apps.Deployer.local_deployer()
    ref = Livebook.Apps.Deployer.deploy_monitor(deployer_pid, app_spec)

    app_pid =
      receive do
        {:deploy_result, ^ref, {:ok, pid}} -> pid
      end

    {slug, app_pid}
  end

  @moduletag authentication: %{mode: :password, secret: "long_livebook_password"}

  # Integration tests for the authentication scenarios

  describe "public app" do
    @describetag app_settings: %{access_type: :public}

    test "does not require authentication", %{conn: conn, slug: slug} do
      {:ok, view, _} =
        conn
        |> live(~p"/apps/#{slug}")
        |> follow_redirect(conn)

      assert render(view) =~ "Untitled notebook"
    end
  end

  describe "protected app" do
    @describetag app_settings: %{access_type: :protected, password: "long_app_password"}

    test "redirect to auth page when not authenticated", %{conn: conn, slug: slug} do
      {:error, {:live_redirect, %{to: to}}} = live(conn, ~p"/apps/#{slug}")
      assert to == "/apps/#{slug}/authenticate"
    end

    test "shows an error on invalid password", %{conn: conn, slug: slug} do
      {:ok, view, _} = live(conn, ~p"/apps/#{slug}/authenticate")

      assert view
             |> element("form")
             |> render_submit(%{password: "invalid password"}) =~ "app password is invalid"
    end

    test "persists authentication across requests", %{conn: conn, slug: slug} do
      {:ok, view, _} = live(conn, ~p"/apps/#{slug}/authenticate")

      view
      |> element("form")
      |> render_submit(%{password: "long_app_password"})

      # The token is stored on the client

      assert_push_event(view, "persist_app_auth", %{"slug" => ^slug, "token" => token})

      assert {:error, {:live_redirect, %{to: to}}} = render_hook(view, "app_auth_persisted")
      assert to == "/apps/#{slug}"

      # Then, the client passes the token in connect params

      conn = put_connect_params(conn, %{"app_auth_token" => token})

      {:ok, view, _} =
        conn
        |> live("/apps/#{slug}")
        |> follow_redirect(conn)

      assert render(view) =~ "Untitled notebook"

      # The auth page redirects to the app

      {:error, {:live_redirect, %{to: to}}} = live(conn, "/apps/#{slug}/authenticate")

      assert to == "/apps/#{slug}"
    end

    test "when redirected from app session page, returns to that same page",
         %{conn: conn, slug: slug} do
      {:ok, %{sessions: [%{id: session_id}]}} = Livebook.Apps.fetch_app(slug)

      # We navigate to a specific session and get redirected to auth

      {:ok, view, _} =
        conn
        |> live(~p"/apps/#{slug}/sessions/#{session_id}")
        |> follow_redirect(conn)

      view
      |> element("form")
      |> render_submit(%{password: "long_app_password"})

      assert_push_event(view, "persist_app_auth", %{"slug" => ^slug, "token" => _token})

      assert {:error, {:live_redirect, %{to: to}}} = render_hook(view, "app_auth_persisted")
      assert to == ~p"/apps/#{slug}/sessions/#{session_id}"
    end

    test "redirects to the app page when authenticating in Livebook", %{conn: conn, slug: slug} do
      conn = get(conn, ~p"/authenticate?redirect_to=/apps/#{slug}")
      assert redirected_to(conn) == "/authenticate"

      conn = post(conn, ~p"/authenticate", password: "long_livebook_password")
      assert redirected_to(conn) == "/apps/#{slug}"

      {:ok, view, _} =
        conn
        |> live(~p"/apps/#{slug}")
        |> follow_redirect(conn)

      assert render(view) =~ "Untitled notebook"

      # The auth page redirects to the app

      {:error, {:live_redirect, %{to: to}}} = live(conn, ~p"/apps/#{slug}/authenticate")
      assert to == "/apps/#{slug}"
    end
  end

  test "redirects to homepage when accessing non-existent app", %{conn: conn} do
    assert {:error, {:redirect, %{to: "/"}}} = live(conn, ~p"/apps/nonexistent")
  end
end
