defmodule Livebook.TeamsTest do
  use Livebook.TeamsIntegrationCase, async: true

  alias Livebook.FileSystem
  alias Livebook.Notebook
  alias Livebook.Teams
  alias Livebook.Utils

  setup :teams

  @moduletag subscribe_to_hubs_topics: [:connection, :file_systems, :secrets]
  @moduletag subscribe_to_teams_topics: [:clients, :deployment_groups, :app_deployments, :agents]

  describe "create_org/1" do
    test "returns the device flow data to confirm the org creation" do
      org = build(:org)

      assert {:ok,
              %{
                "device_code" => _device_code,
                "expires_in" => 300,
                "id" => _org_id,
                "user_code" => _user_code,
                "verification_uri" => _verification_uri
              }} = Teams.create_org(org, %{})
    end

    test "returns changeset errors when data is invalid" do
      org = build(:org)

      assert {:error, changeset} = Teams.create_org(org, %{name: nil})
      assert "can't be blank" in errors_on(changeset).name
    end
  end

  describe "join_org/1" do
    test "returns the device flow data to confirm the org creation", %{node: node} do
      org = build(:org)
      key_hash = Teams.Org.key_hash(org)
      teams_org = TeamsRPC.create_org(node, name: org.name)
      user = TeamsRPC.create_user(node)

      TeamsRPC.create_org_key(node, org: teams_org, key_hash: key_hash)
      TeamsRPC.create_user_org(node, org: teams_org, user: user)

      assert {:ok,
              %{
                "device_code" => _device_code,
                "expires_in" => 300,
                "id" => _org_id,
                "user_code" => _user_code,
                "verification_uri" => _verification_uri
              }} = Teams.join_org(org, %{})
    end

    test "returns changeset errors when data is invalid" do
      org = build(:org)

      assert {:error, changeset} = Teams.join_org(org, %{name: nil})
      assert "can't be blank" in errors_on(changeset).name
    end

    test "returns changeset errors when org doesn't exist" do
      org = build(:org)

      assert {:error, changeset} = Teams.join_org(org, %{})
      assert "does not exist" in errors_on(changeset).name
      assert "does not match existing key" in errors_on(changeset).teams_key
    end
  end

  describe "get_org_request_completion_data/1" do
    test "returns the org data when it has been confirmed", %{node: node} do
      teams_key = Teams.Org.teams_key()
      key_hash = :crypto.hash(:sha256, teams_key) |> Base.url_encode64(padding: false)

      user = TeamsRPC.create_user(node)
      org_request = TeamsRPC.create_org_request(node, key_hash: key_hash)
      org_request = TeamsRPC.confirm_org_request(node, org_request, user)

      org =
        build(:org,
          id: org_request.id,
          name: org_request.name,
          teams_key: teams_key,
          user_code: org_request.user_code
        )

      %{
        token: token,
        user_org: %{
          org: %{
            id: id,
            name: name,
            keys: [%{id: org_key_id}],
            key_pair: %{public_key: org_public_key}
          },
          user: %{id: user_id}
        }
      } = org_request.user_org_session

      assert Teams.get_org_request_completion_data(org, org_request.device_code) ==
               {:ok,
                %{
                  "id" => id,
                  "name" => name,
                  "org_key_id" => org_key_id,
                  "org_public_key" => org_public_key,
                  "session_token" => token,
                  "user_id" => user_id
                }}
    end

    test "returns the org request awaiting confirmation", %{node: node} do
      teams_key = Teams.Org.teams_key()
      key_hash = :crypto.hash(:sha256, teams_key) |> Base.url_encode64(padding: false)
      org_request = TeamsRPC.create_org_request(node, key_hash: key_hash)

      org =
        build(:org,
          id: org_request.id,
          name: org_request.name,
          teams_key: teams_key,
          user_code: org_request.user_code
        )

      assert Teams.get_org_request_completion_data(org, org_request.device_code) ==
               {:ok, :awaiting_confirmation}
    end

    test "returns error when org request doesn't exist" do
      org = build(:org, id: 0)

      assert {:transport_error, _embarrassing} =
               Teams.get_org_request_completion_data(org, "")
    end

    test "returns error when org request expired", %{node: node} do
      now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
      expires_at = NaiveDateTime.add(now, -5000)
      teams_key = Teams.Org.teams_key()
      key_hash = :crypto.hash(:sha256, teams_key) |> Base.url_encode64(padding: false)
      org_request = TeamsRPC.create_org_request(node, expires_at: expires_at, key_hash: key_hash)

      org =
        build(:org,
          id: org_request.id,
          name: org_request.name,
          teams_key: teams_key,
          user_code: org_request.user_code
        )

      assert Teams.get_org_request_completion_data(org, org_request.device_code) ==
               {:error, :expired}
    end
  end

  describe "create_deployment_group/2" do
    @describetag teams_for: :user

    test "creates a new deployment group when the data is valid", %{team: team} do
      attrs = params_for(:deployment_group, name: "DEPLOYMENT_GROUP_#{team.id}", mode: :online)

      assert {:ok, %{id: id, name: name, mode: mode}} = Teams.create_deployment_group(team, attrs)
      assert_receive {:deployment_group_created, %{id: ^id, name: ^name, mode: ^mode}}

      # Guarantee uniqueness
      assert {:error, changeset} = Teams.create_deployment_group(team, attrs)
      assert "has already been taken" in errors_on(changeset).name
    end

    test "creates a new deployment group with Livebook Teams authentication", %{team: team} do
      attrs = params_for(:deployment_group, name: "DEPLOYMENT_GROUP_#{team.id}", mode: :online)

      assert {:ok, %{id: id, name: name, mode: mode, teams_auth: true}} =
               Teams.create_deployment_group(team, attrs)

      assert_receive {:deployment_group_created,
                      %{id: ^id, name: ^name, mode: ^mode, teams_auth: true}}
    end

    test "returns changeset errors with invalid data", %{team: team} do
      attrs = params_for(:deployment_group, name: "")

      assert {:error, changeset} = Teams.create_deployment_group(team, attrs)
      assert "can't be blank" in errors_on(changeset).name

      attrs = params_for(:deployment_group, mode: "invalid")

      assert {:error, changeset} = Teams.create_deployment_group(team, attrs)
      assert "is invalid" in errors_on(changeset).mode
    end
  end

  describe "deploy_app/2" do
    @describetag teams_for: :user

    @tag :tmp_dir
    test "deploys app to Teams from a notebook", %{team: team, node: node, tmp_dir: tmp_dir} do
      attrs = params_for(:deployment_group, name: "BAZ", mode: :online)
      {:ok, %{id: id}} = Teams.create_deployment_group(team, attrs)
      assert_receive {:deployment_group_created, %{id: ^id}}

      # creates the app deployment
      slug = Utils.random_short_id()
      title = "MyNotebook-#{slug}"
      app_settings = %{Notebook.AppSettings.new() | slug: slug}

      notebook = %{
        Notebook.new()
        | app_settings: app_settings,
          name: title,
          hub_id: team.id,
          deployment_group_id: id
      }

      files_dir = FileSystem.File.local(tmp_dir)
      assert {:ok, app_deployment} = Teams.AppDeployment.new(notebook, files_dir)
      assert Teams.deploy_app(team, app_deployment) == :ok

      sha = app_deployment.sha
      multi_session = app_settings.multi_session
      access_type = app_settings.access_type

      assert_receive {:app_deployment_started,
                      %Livebook.Teams.AppDeployment{
                        slug: ^slug,
                        sha: ^sha,
                        title: ^title,
                        multi_session: ^multi_session,
                        access_type: ^access_type,
                        deployment_group_id: ^id
                      } = app_deployment2}

      assert {:error,
              %{errors: [slug: {"should only contain alphanumeric characters and dashes", []}]}} =
               Teams.deploy_app(team, %{app_deployment | slug: "@abc"})

      assert {:error, %{errors: [multi_session: {"can't be blank", []}]}} =
               Teams.deploy_app(team, %{app_deployment | multi_session: nil})

      assert {:error, %{errors: [access_type: {"can't be blank", []}]}} =
               Teams.deploy_app(team, %{app_deployment | access_type: nil})

      assert {:error, %{errors: [access_type: {"is invalid", []}]}} =
               Teams.deploy_app(team, %{app_deployment | access_type: :abc})

      # force app deployment to be stopped
      TeamsRPC.toggle_app_deployment(node, app_deployment2.id, team.org_id)
      assert_receive {:app_deployment_stopped, ^app_deployment2}
    end
  end

  describe "fetch_cli_session/1" do
    @describetag teams_for: :cli

    @tag teams_persisted: false
    test "authenticates the org token", %{team: team} do
      config = %{teams_key: team.teams_key, session_token: team.session_token}

      refute Livebook.Hubs.hub_exists?(team.id)
      assert Teams.fetch_cli_session(config) == {:ok, team}
      assert Livebook.Hubs.hub_exists?(team.id)
    end

    @tag teams_for: :user
    test "authenticates the org token when hub already exists",
         %{team: team, org: org, node: node} do
      {key, _} = TeamsRPC.create_org_token(node, org: org)
      config = %{teams_key: team.teams_key, session_token: key}

      assert Teams.fetch_cli_session(config) ==
               {:ok,
                %Livebook.Hubs.Team{
                  billing_status: team.billing_status,
                  hub_emoji: "🚀",
                  hub_name: team.hub_name,
                  id: team.id,
                  offline: nil,
                  org_id: team.org_id,
                  org_key_id: team.org_key_id,
                  org_public_key: team.org_public_key,
                  session_token: key,
                  teams_key: team.teams_key,
                  user_id: nil
                }}

      assert Livebook.Hubs.hub_exists?(team.id)
      assert Livebook.Hubs.fetch_hub!(team.id).session_token == key
    end

    @tag teams_persisted: false
    test "returns error with invalid credentials", %{team: team} do
      config = %{teams_key: team.teams_key, session_token: "lb_ot_foo"}

      assert {:transport_error, "You are not authorized" <> _} = Teams.fetch_cli_session(config)
      refute Livebook.Hubs.hub_exists?(team.id)
    end
  end

  describe "deploy_app_from_cli/2" do
    @describetag teams_for: :user

    @tag :tmp_dir
    test "deploys app to Teams using a CLI session",
         %{team: team, node: node, tmp_dir: tmp_dir, org: org} do
      %{id: id} =
        TeamsRPC.create_deployment_group(node,
          name: "angry-cat-#{Ecto.UUID.generate()}",
          url: "http://localhost:4123",
          mode: :online,
          org: org
        )

      id = to_string(id)
      hub_id = "team-#{org.name}"
      slug = Utils.random_short_id()
      title = "MyNotebook-#{slug}"
      app_settings = %{Notebook.AppSettings.new() | slug: slug}

      notebook = %{
        Notebook.new()
        | app_settings: app_settings,
          name: title,
          hub_id: hub_id,
          deployment_group_id: id
      }

      files_dir = FileSystem.File.local(tmp_dir)

      # stamp the notebook
      assert {:ok, app_deployment} = Teams.AppDeployment.new(notebook, files_dir)

      # fetch the cli session
      {key, _org_token} = TeamsRPC.create_org_token(node, org: org)
      config = %{teams_key: team.teams_key, session_token: key}
      assert {:ok, team} = Teams.fetch_cli_session(config)

      # deploy the app
      assert {:ok, _url} = Teams.deploy_app_from_cli(team, app_deployment, id)

      sha = app_deployment.sha
      multi_session = app_settings.multi_session
      access_type = app_settings.access_type

      assert_receive {:app_deployment_started,
                      %Livebook.Teams.AppDeployment{
                        slug: ^slug,
                        sha: ^sha,
                        title: ^title,
                        deployed_by: "CLI",
                        multi_session: ^multi_session,
                        access_type: ^access_type,
                        deployment_group_id: ^id
                      } = app_deployment2}

      assert Teams.deploy_app_from_cli(team, app_deployment, 999) ==
               {:error, %{"deployment_group_id" => ["does not exist"]}}

      assert Teams.deploy_app_from_cli(team, %{app_deployment | slug: "@abc"}, id) ==
               {:error, %{"slug" => ["should only contain alphanumeric characters and dashes"]}}

      assert Teams.deploy_app_from_cli(team, %{app_deployment | multi_session: nil}, id) ==
               {:error, %{"multi_session" => ["can't be blank"]}}

      assert Teams.deploy_app_from_cli(team, %{app_deployment | access_type: nil}, id) ==
               {:error, %{"access_type" => ["can't be blank"]}}

      assert Teams.deploy_app_from_cli(team, %{app_deployment | access_type: :abc}, id) ==
               {:error, %{"access_type" => ["is invalid"]}}

      # force app deployment to be stopped
      TeamsRPC.toggle_app_deployment(node, app_deployment2.id, team.org_id)
      assert_receive {:app_deployment_stopped, ^app_deployment2}
    end
  end
end
