defmodule LivebookWeb.Integration.SessionLiveTest do
  use Livebook.TeamsIntegrationCase, async: true

  import Phoenix.LiveViewTest
  import Livebook.SessionHelpers

  alias Livebook.FileSystem
  alias Livebook.Sessions
  alias Livebook.Session

  setup do
    {:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new())
    Session.subscribe(session.id)

    on_exit(fn ->
      Session.close(session.pid)
    end)

    %{session: session}
  end

  describe "hubs" do
    test "selects the notebook hub", %{conn: conn, user: user, node: node, session: session} do
      hub = create_team_hub(user, node)
      id = hub.id
      personal_id = Livebook.Hubs.Personal.id()

      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")

      assert Session.get_notebook(session.pid).hub_id == personal_id

      view
      |> element(~s/#select-hub-#{id}/)
      |> render_click()

      assert_receive {:operation, {:set_notebook_hub, _, ^id}}
      assert Session.get_notebook(session.pid).hub_id == hub.id
    end

    test "closes all sessions from notebooks that belongs to the org when the org deletes the user",
         %{conn: conn, user: user, node: node, session: session} do
      Livebook.Hubs.Broadcasts.subscribe([:connection, :crud, :secrets])
      Livebook.Teams.Broadcasts.subscribe([:clients])
      Session.subscribe(session.id)

      hub = create_team_hub(user, node)
      id = hub.id

      assert_receive {:hub_connected, ^id}
      assert_receive {:client_connected, ^id}, 10_000

      Session.set_notebook_hub(session.pid, id)

      assert_receive {:operation, {:set_notebook_hub, _, ^id}}
      assert Session.get_notebook(session.pid).hub_id == id

      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
      assert has_element?(view, ~s/#select-hub-#{id}/)

      # force user to be deleted from org
      erpc_call(node, :delete_user_org, [user.id, hub.org_id])
      reason = "#{hub.hub_name}: you were removed from the org"

      # checks if the hub received the `user_deleted` event and deleted the hub
      assert_receive {:hub_server_error, ^id, ^reason}
      assert_receive {:hub_deleted, ^id}
      refute hub in Livebook.Hubs.get_hubs()

      # all sessions that uses the deleted hub must be closed
      assert_receive :session_closed
    end
  end

  describe "secrets" do
    test "creates a new secret", %{conn: conn, user: user, node: node, session: session} do
      team = create_team_hub(user, node)

      # loads the session page
      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")

      # selects the notebook's hub with team hub id
      view
      |> element(~s/#select-hub-#{team.id}/)
      |> render_click()

      # clicks the button to add a new secret
      view
      |> element("#new-secret-button")
      |> render_click(%{})

      # redirects to secrets action to
      # render the secret modal
      assert_patch(view, ~p"/sessions/#{session.id}/secrets")

      secret = build(:secret, hub_id: team.id)

      attrs = %{
        secret: %{
          name: secret.name,
          value: secret.value,
          hub_id: team.id
        }
      }

      # fills and submits the secrets modal form
      # to create a new secret on team hub
      form = element(view, ~s{#secrets-modal form[phx-submit="save"]})

      render_change(form, attrs)
      render_submit(form, attrs)

      # receives the operation event
      assert_receive {:operation, {:sync_hub_secrets, "__server__"}}
      assert secret in Livebook.Hubs.get_secrets(team)

      # checks the secret on the UI
      assert_session_secret(view, session.pid, secret, :hub_secrets)
    end

    test "redirects the user to update or delete a secret",
         %{conn: conn, user: user, node: node, session: session} do
      Livebook.Hubs.Broadcasts.subscribe([:secrets, :connection])
      team = create_team_hub(user, node)
      id = team.id
      assert_receive {:hub_connected, ^id}

      # creates a secret
      secret = insert_secret(hub_id: team.id)
      assert_receive {:secret_created, ^secret}

      # selects the notebook's hub with team hub id
      Session.set_notebook_hub(session.pid, team.id)

      # loads the session page
      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")

      # clicks the button to edit a secret
      view
      |> element("#hub-#{id}-secret-#{secret.name}-edit-button")
      |> render_click()

      # redirects to hub page and loads the modal with
      # the secret name and value filled
      assert_redirect(view, ~p"/hub/#{id}/secrets/edit/#{secret.name}")
    end

    test "toggle a secret from team hub", %{conn: conn, session: session, user: user, node: node} do
      team = create_team_hub(user, node)

      # loads the session page
      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")

      # selects the notebook's hub with team hub id
      Session.set_notebook_hub(session.pid, team.id)

      # creates a new secret
      secret = build(:secret, hub_id: team.id)

      assert Livebook.Hubs.create_secret(team, secret) == :ok

      # receives the operation event
      assert_receive {:operation, {:sync_hub_secrets, "__server__"}}
      assert secret in Livebook.Hubs.get_secrets(team)

      # checks the secret on the UI
      Session.set_secret(session.pid, secret)
      assert_session_secret(view, session.pid, secret)
    end

    test "adding a missing secret using 'Add secret' button",
         %{conn: conn, user: user, node: node, session: session} do
      team = create_team_hub(user, node)

      secret = build(:secret, hub_id: team.id)

      # selects the notebook's hub with team hub id
      Session.set_notebook_hub(session.pid, team.id)

      # executes the code to trigger the `System.EnvError` exception
      # and outputs the 'Add secret' button
      section_id = insert_section(session.pid)
      code = ~s{System.fetch_env!("LB_#{secret.name}")}
      cell_id = insert_text_cell(session.pid, section_id, :code, code)

      Session.queue_cell_evaluation(session.pid, cell_id)
      assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}

      # enters the session to check if the button exists
      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
      expected_url = ~p"/sessions/#{session.id}/secrets?secret_name=#{secret.name}"
      add_secret_button = element(view, "a[href='#{expected_url}']")
      assert has_element?(add_secret_button)

      # clicks the button and fills the form to create a new secret
      # that prefilled the name with the received from exception.
      render_click(add_secret_button)
      form_element = element(view, "#secrets-modal form[phx-submit='save']")
      assert has_element?(form_element)
      attrs = %{value: secret.value, hub_id: team.id}
      render_submit(form_element, %{secret: attrs})

      # receives the operation event
      assert_receive {:operation, {:sync_hub_secrets, "__server__"}}
      assert secret in Livebook.Hubs.get_secrets(team)

      # checks if the secret exists and is inside the session,
      # then executes the code cell again and checks if the
      # secret value is what we expected.
      assert_session_secret(view, session.pid, secret, :hub_secrets)
      Session.queue_cell_evaluation(session.pid, cell_id)

      assert_receive {:operation,
                      {:add_cell_evaluation_response, _, ^cell_id,
                       %{type: :terminal_text, text: output}, _}}

      assert output == "\e[32m\"#{secret.value}\"\e[0m"
    end

    test "granting access for missing secret using 'Add secret' button",
         %{conn: conn, user: user, node: node, session: session} do
      team = create_team_hub(user, node)

      secret = build(:secret, hub_id: team.id)

      # selects the notebook's hub with team hub id
      Session.set_notebook_hub(session.pid, team.id)

      # executes the code to trigger the `System.EnvError` exception
      # and outputs the 'Add secret' button
      section_id = insert_section(session.pid)
      code = ~s{System.fetch_env!("LB_#{secret.name}")}
      cell_id = insert_text_cell(session.pid, section_id, :code, code)

      Session.queue_cell_evaluation(session.pid, cell_id)
      assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}

      # enters the session to check if the button exists
      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
      expected_url = ~p"/sessions/#{session.id}/secrets?secret_name=#{secret.name}"
      add_secret_button = element(view, "a[href='#{expected_url}']")
      assert has_element?(add_secret_button)

      # creates the secret
      assert Livebook.Hubs.create_secret(team, secret) == :ok

      # receives the operation event
      assert_receive {:operation, {:sync_hub_secrets, "__server__"}}
      assert secret in Livebook.Hubs.get_secrets(team)

      # remove the secret from session
      Session.unset_secret(session.pid, secret.name)

      # clicks the button and checks if the 'Grant access' banner
      # is being shown, so clicks it's button to set the app secret
      # to the session, allowing the user to fetches the secret.
      render_click(add_secret_button)

      assert render(view) =~
               "in the #{hub_label(team)} workspace. Allow this notebook to access it?"

      view
      |> element("#secrets-modal button", "Grant access")
      |> render_click()

      # checks if the secret exists and is inside the session,
      # then executes the code cell again and checks if the
      # secret value is what we expected.
      assert_session_secret(view, session.pid, secret, :hub_secrets)
      Session.queue_cell_evaluation(session.pid, cell_id)

      assert_receive {:operation,
                      {:add_cell_evaluation_response, _, ^cell_id,
                       %{type: :terminal_text, text: output}, _}}

      assert output == "\e[32m\"#{secret.value}\"\e[0m"
    end
  end

  describe "files" do
    test "shows only hub's file systems",
         %{conn: conn, user: user, node: node, session: session} do
      Livebook.Hubs.Broadcasts.subscribe([:file_systems])

      personal_id = Livebook.Hubs.Personal.id()
      personal_file_system = build(:fs_s3)
      Livebook.Hubs.Personal.save_file_system(personal_file_system)

      team = create_team_hub(user, node)
      team_id = team.id

      bucket_url = "https://my-own-bucket.s3.amazonaws.com"

      team_file_system =
        build(:fs_s3,
          id: FileSystem.S3.id(team_id, bucket_url),
          bucket_url: bucket_url,
          hub_id: team_id
        )

      Livebook.Hubs.create_file_system(team, team_file_system)
      assert_receive {:file_system_created, %{hub_id: ^team_id} = team_file_system}

      # loads the session page
      {:ok, view, _html} = live(conn, ~p"/sessions/#{session.id}/add-file/storage")

      # change the hub to Personal
      # and checks the file systems from Personal
      Session.set_notebook_hub(session.pid, personal_id)
      assert_receive {:operation, {:set_notebook_hub, _client, ^personal_id}}

      file_entry_select = element(view, "#add-file-entry-select")

      # checks the file systems from Personal
      assert render(file_entry_select) =~ "local"
      assert render(file_entry_select) =~ personal_file_system.id
      refute render(file_entry_select) =~ team_file_system.id

      # change the hub to Team
      # and checks the file systems from Team
      Session.set_notebook_hub(session.pid, team.id)
      assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}

      assert render(file_entry_select) =~ "local"
      refute render(file_entry_select) =~ personal_file_system.id
      assert render(file_entry_select) =~ team_file_system.id
    end

    test "shows file system from offline hub", %{conn: conn, session: session} do
      Livebook.Hubs.Broadcasts.subscribe([:file_systems])

      hub = offline_hub()
      hub_id = hub.id
      bucket_url = "https://#{hub.id}-file-system.s3.amazonaws.com"

      file_system =
        build(:fs_s3,
          id: FileSystem.S3.id(hub_id, bucket_url),
          bucket_url: bucket_url,
          hub_id: hub_id,
          external_id: "123"
        )

      put_offline_hub_file_system(file_system)
      assert_receive {:file_system_created, ^file_system}

      # loads the session page
      {:ok, view, _html} = live(conn, ~p"/sessions/#{session.id}/add-file/storage")

      # change the hub to Personal
      # and checks the file systems from Offline hub
      Session.set_notebook_hub(session.pid, hub_id)
      assert_receive {:operation, {:set_notebook_hub, _client, ^hub_id}}

      # checks the file systems from Offline hub
      file_entry_select = element(view, "#add-file-entry-select")
      assert render(file_entry_select) =~ "local"
      assert render(file_entry_select) =~ file_system.id

      remove_offline_hub_file_system(file_system)
    end
  end

  describe "offline deployment with docker" do
    @tag :tmp_dir
    test "show deployment group on app deployment",
         %{conn: conn, user: user, node: node, session: session, tmp_dir: tmp_dir} do
      team = create_team_hub(user, node)
      team_id = team.id

      insert_deployment_group(
        name: "DEPLOYMENT_GROUP_SUSIE",
        mode: :online,
        hub_id: team_id
      )

      Session.set_notebook_hub(session.pid, team_id)
      assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}

      notebook_path = Path.join(tmp_dir, "notebook.livemd")
      file = FileSystem.File.local(notebook_path)
      Session.set_file(session.pid, file)

      slug = Livebook.Utils.random_short_id()
      app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
      Session.set_app_settings(session.pid, app_settings)

      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-docker")

      assert render(view) =~ "Deployment Group"
      assert has_element?(view, "#select_deployment_group_form")
    end

    @tag :tmp_dir
    test "set deployment group on app deployment",
         %{conn: conn, user: user, node: node, session: session, tmp_dir: tmp_dir} do
      team = create_team_hub(user, node)
      team_id = team.id

      insert_deployment_group(
        name: "DEPLOYMENT_GROUP_SUSIE",
        mode: :online,
        hub_id: team_id
      )

      deployment_group =
        insert_deployment_group(
          name: "DEPLOYMENT_GROUP_TOBIAS",
          mode: :online,
          hub_id: team_id
        )

      Session.set_notebook_hub(session.pid, team_id)
      assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}

      notebook_path = Path.join(tmp_dir, "notebook.livemd")
      file = FileSystem.File.local(notebook_path)
      Session.set_file(session.pid, file)

      slug = Livebook.Utils.random_short_id()
      app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
      Session.set_app_settings(session.pid, app_settings)

      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-docker")

      assert render(view) =~ "Deployment Group"
      assert has_element?(view, "#select_deployment_group_form")

      id = deployment_group.id

      view
      |> form("#select_deployment_group_form", %{deployment_group: %{id: id}})
      |> render_change()

      assert_receive {:operation, {:set_notebook_deployment_group, _client, ^id}}
    end

    @tag :tmp_dir
    test "show no deployments groups available",
         %{conn: conn, user: user, node: node, session: session, tmp_dir: tmp_dir} do
      team = create_team_hub(user, node)
      team_id = team.id

      Session.set_notebook_hub(session.pid, team_id)
      assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}

      notebook_path = Path.join(tmp_dir, "notebook.livemd")
      file = FileSystem.File.local(notebook_path)
      Session.set_file(session.pid, file)

      slug = Livebook.Utils.random_short_id()
      app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
      Session.set_app_settings(session.pid, app_settings)

      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-docker")

      assert render(view) =~ "Deployment Group"
      assert render(view) =~ "None configured"
      refute has_element?(view, "#select_deployment_group_form")
    end
  end

  describe "online deployment" do
    test "shows a message when non-teams hub is selected",
         %{conn: conn, user: user, node: node, session: session} do
      create_team_hub(user, node)

      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")

      view
      |> element("a", "Deploy with Livebook Teams")
      |> render_click()

      assert render(view) =~
               "In order to deploy your app using Livebook Teams, you need to select"
    end

    test "deployment flow with no deployment groups in the hub",
         %{conn: conn, user: user, node: node, session: session} do
      team = create_team_hub(user, node)
      Session.set_notebook_hub(session.pid, team.id)

      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")

      view
      |> element("a", "Deploy with Livebook Teams")
      |> render_click()

      # Step: configuring valid app settings

      assert render(view) =~ "You must configure your app before deploying it."

      slug = Livebook.Utils.random_short_id()

      view
      |> element(~s/#app-settings-modal form/)
      |> render_submit(%{"app_settings" => %{"slug" => slug}})

      # From this point forward we are in a child LV
      view = find_live_child(view, "app-teams")
      assert render(view) =~ "App deployment with Livebook Teams"

      # Step: deployment group creation

      assert render(view) =~ "Step: add deployment group"
      assert render(view) =~ "You must create a deployment group before deploying the app."

      view
      |> element(~s/#add-deployment-group-form/)
      |> render_submit(%{"deployment_group" => %{"name" => "test"}})

      # Step: agent instance setup

      assert render(view) =~ "Step: add app server"
      assert render(view) =~ "You must set up an app server for the app to run on."

      assert render(view) =~ "Awaiting an app server to be set up."

      [deployment_group] = Livebook.Hubs.TeamClient.get_deployment_groups(team.id)
      simulate_agent_join(team, deployment_group)

      assert render(view) =~ "An app server is running"

      # Step: deploy

      view
      |> element("button", "Deploy")
      |> render_click()

      assert render(view) =~
               "App deployment created successfully"

      assert render(view) =~ "#{Livebook.Config.teams_url()}/orgs/#{team.org_id}"
    end

    test "deployment flow with existing deployment groups in the hub",
         %{conn: conn, user: user, node: node, session: session} do
      Livebook.Teams.Broadcasts.subscribe([:deployment_groups])
      team = create_team_hub(user, node)
      Session.set_notebook_hub(session.pid, team.id)

      id = insert_deployment_group(mode: :online, hub_id: team.id).id
      assert_receive {:deployment_group_created, %{id: ^id} = deployment_group}

      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")

      view
      |> element("a", "Deploy with Livebook Teams")
      |> render_click()

      # Step: configuring valid app settings

      assert render(view) =~ "You must configure your app before deploying it."

      slug = Livebook.Utils.random_short_id()

      view
      |> element(~s/#app-settings-modal form/)
      |> render_submit(%{"app_settings" => %{"slug" => slug}})

      # From this point forward we are in a child LV
      view = find_live_child(view, "app-teams")
      assert render(view) =~ "App deployment with Livebook Teams"

      # Step: selecting deployment group

      view
      |> element(~s/[phx-click="select_deployment_group"][phx-value-id="#{deployment_group.id}"]/)
      |> render_click()

      assert_receive {:operation, {:set_notebook_deployment_group, _, ^id}}
      assert render(view) =~ "The selected deployment group has no app servers."

      view
      |> element(~s/button/, "Add app server")
      |> render_click()

      # Step: agent instance setup

      assert render(view) =~ "Step: add app server"
      assert render(view) =~ "Awaiting an app server to be set up."

      [deployment_group] = Livebook.Hubs.TeamClient.get_deployment_groups(team.id)
      simulate_agent_join(team, deployment_group)

      assert render(view) =~ "An app server is running"

      # Step: deploy

      view
      |> element("button", "Deploy")
      |> render_click()

      assert render(view) =~
               "App deployment created successfully"
    end

    test "shows an error when the deployment size is higher than the maximum size of 20MB",
         %{conn: conn, user: user, node: node, session: session} do
      Livebook.Teams.Broadcasts.subscribe([:deployment_groups])
      team = create_team_hub(user, node)
      Session.set_notebook_hub(session.pid, team.id)

      slug = Livebook.Utils.random_short_id()
      app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
      Session.set_app_settings(session.pid, app_settings)

      id = insert_deployment_group(mode: :online, hub_id: team.id).id
      assert_receive {:deployment_group_created, %{id: ^id}}

      Session.set_notebook_deployment_group(session.pid, id)
      assert_receive {:operation, {:set_notebook_deployment_group, _, ^id}}

      %{files_dir: files_dir} = session
      image_file = FileSystem.File.resolve(files_dir, "image.jpg")
      :ok = FileSystem.File.write(image_file, :crypto.strong_rand_bytes(20 * 1024 * 1024))
      Session.add_file_entries(session.pid, [%{type: :attachment, name: "image.jpg"}])

      {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-teams")

      # From this point forward we are in a child LV
      view = find_live_child(view, "app-teams")
      assert render(view) =~ "App deployment with Livebook Teams"

      view
      |> element("button", "Deploy")
      |> render_click()

      assert render(view) =~
               "Failed to pack files: the notebook and its attachments have exceeded the maximum size of 20MB"
    end
  end
end
