defmodule AsciinemaWeb.Api.RecordingControllerTest do
  use AsciinemaWeb.ConnCase, async: true
  import Asciinema.Factory
  alias Asciinema.Accounts
  alias Asciinema.AppEnv

  setup(context) do
    [token: Map.get(context, :token, "9da34ff4-9bf7-45d4-aa88-98c933b15a3f")]
  end

  @recording_url ~r|^http://localhost:4001/a/[a-zA-Z0-9]{25}|
  @successful_response ~r|View.+at.+http://localhost:4001/a/[a-zA-Z0-9]{25}\n|s

  describe "create without authentication" do
    test "fails", %{conn: conn} do
      upload = fixture(:upload, %{path: "3/minimal.cast"})

      conn = upload(conn, upload)

      assert %{"type" => "unauthenticated", "message" => "Missing install ID"} =
               json_response(conn, 401)
    end
  end

  describe "create with invalid install ID" do
    setup [:authenticate]

    @tag token: "invalid-lol"
    test "fails", %{conn: conn} do
      upload = fixture(:upload, %{path: "3/minimal.cast"})

      conn = upload(conn, upload)

      assert %{"type" => "unauthenticated", "message" => "Invalid install ID"} =
               json_response(conn, 401)
    end
  end

  describe "create with revoked install ID" do
    setup [:register_cli, :revoke_cli, :authenticate]

    test "fails", %{conn: conn} do
      upload = fixture(:upload, %{path: "3/minimal.json"})

      conn = upload(conn, upload)

      assert %{"type" => "unauthenticated", "message" => "Revoked CLI"} = json_response(conn, 401)
    end
  end

  describe "create via legacy path" do
    setup [:authenticate]

    test "succeeds", %{conn: conn} do
      upload = fixture(:upload, %{path: "2/minimal.cast"})

      conn =
        conn
        |> put_resp_content_type("application/json")
        |> post(~p"/api/asciicasts", %{"asciicast" => upload})

      assert List.first(get_resp_header(conn, "location")) =~ @recording_url
      assert %{"url" => url, "message" => message} = json_response(conn, 201)
      assert url =~ @recording_url
      assert message =~ @successful_response
    end
  end

  describe "create with authentication" do
    setup [:authenticate]

    test "json file, v1 format succeeds", %{conn: conn} do
      upload = fixture(:upload, %{path: "1/full.json"})

      conn = upload(conn, upload)

      assert List.first(get_resp_header(conn, "location")) =~ @recording_url

      assert %{
               "id" => id,
               "url" => url,
               "file_url" => file_url,
               "audio_url" => nil,
               "title" => "bashing :)",
               "description" => nil,
               "visibility" => "unlisted",
               "message" => message
             } = json_response(conn, 201)

      assert is_integer(id)
      assert url =~ @recording_url
      assert file_url =~ ~r/^http.+\.json$/
      assert message =~ @successful_response
    end

    test "json file, v2 format, minimal succeeds", %{conn: conn} do
      upload = fixture(:upload, %{path: "2/minimal.cast"})

      conn = upload(conn, upload)

      assert List.first(get_resp_header(conn, "location")) =~ @recording_url

      assert %{
               "id" => id,
               "url" => url,
               "file_url" => file_url,
               "audio_url" => nil,
               "title" => nil,
               "description" => nil,
               "visibility" => "unlisted",
               "message" => message
             } = json_response(conn, 201)

      assert is_integer(id)
      assert url =~ @recording_url
      assert file_url =~ ~r/^http.+\.cast$/
      assert message =~ @successful_response
    end

    test "json file, v2 format, full succeeds", %{conn: conn} do
      upload = fixture(:upload, %{path: "2/full.cast"})

      conn = upload(conn, upload)

      assert List.first(get_resp_header(conn, "location")) =~ @recording_url

      assert %{
               "id" => id,
               "url" => url,
               "file_url" => file_url,
               "audio_url" => nil,
               "title" => "bashing :)",
               "description" => nil,
               "visibility" => "unlisted",
               "message" => message
             } = json_response(conn, 201)

      assert is_integer(id)
      assert url =~ @recording_url
      assert file_url =~ ~r/^http.+\.cast$/
      assert message =~ @successful_response
    end

    test "json file, v1 format, missing required data fails", %{conn: conn} do
      upload = fixture(:upload, %{path: "1/invalid.json"})

      conn = upload(conn, upload)

      assert %{
               "type" => "validation_failed",
               "message" => "Validation failed",
               "details" => [%{"field" => "term_cols", "message" => "can't be blank"}]
             } = json_response(conn, 422)
    end

    test "json file, v2 format, invalid theme format fails", %{conn: conn} do
      upload = fixture(:upload, %{path: "2/invalid-theme.cast"})

      conn = upload(conn, upload)

      assert %{
               "type" => "validation_failed",
               "message" => "Validation failed",
               "details" => [%{"field" => "term_theme_bg"}, %{"field" => "term_theme_fg"}]
             } = json_response(conn, 422)
    end

    test "json file, unsupported version number fails", %{conn: conn} do
      upload = fixture(:upload, %{path: "5/asciicast.json"})

      conn = upload(conn, upload)

      assert %{
               "type" => "validation_failed",
               "message" => "asciicast v5 is not supported"
             } = json_response(conn, 422)
    end

    test "non-json file fails", %{conn: conn} do
      upload = fixture(:upload, %{path: "favicon.png"})

      conn = upload(conn, upload)

      assert %{
               "type" => "validation_failed",
               "message" => "This doesn't look like a valid asciicast file"
             } = json_response(conn, 422)
    end

    test "requesting json response succeeds", %{conn: conn} do
      upload = fixture(:upload, %{path: "2/minimal.cast"})
      conn = put_req_header(conn, "accept", "application/json")

      conn = upload(conn, upload)

      assert %{"url" => "http" <> _} = json_response(conn, 201)
      assert List.first(get_resp_header(conn, "location")) =~ @recording_url
    end
  end

  describe "create with registered cli when registration is not required" do
    setup [:register_cli, :authenticate]

    test "succeeds", %{conn: conn} do
      upload = fixture(:upload, %{path: "1/full.json"})

      conn = upload(conn, upload)
      response = json_response(conn, 201)

      assert response["url"] =~ @recording_url
      assert response["message"] =~ @successful_response
      assert List.first(get_resp_header(conn, "location")) =~ @recording_url
    end
  end

  describe "create with registered cli when registration is required" do
    setup [:require_registered_cli, :register_cli, :authenticate]

    test "succeeds", %{conn: conn} do
      upload = fixture(:upload, %{path: "2/minimal.cast"})

      conn = upload(conn, upload)
      response = json_response(conn, 201)

      assert response["url"] =~ @recording_url
      assert response["message"] =~ @successful_response
      assert List.first(get_resp_header(conn, "location")) =~ @recording_url
    end
  end

  describe "create with non-registered cli when registration is required" do
    setup [:require_registered_cli, :authenticate]

    test "fails", %{conn: conn} do
      upload = fixture(:upload, %{path: "2/minimal.cast"})

      conn = upload(conn, upload)

      assert %{"type" => "unauthenticated", "message" => "Unregistered CLI"} =
               json_response(conn, 401)
    end
  end

  describe "update without authentication" do
    test "fails", %{conn: conn} do
      asciicast = insert(:asciicast)

      conn = put(conn, ~p"/api/v1/recordings/#{asciicast.id}", %{"title" => "New Title"})

      assert %{"type" => "unauthenticated", "message" => "Missing install ID"} =
               json_response(conn, 401)
    end
  end

  describe "update with unknown install ID" do
    setup [:authenticate]

    test "fails", %{conn: conn} do
      asciicast = insert(:asciicast)

      conn = put(conn, ~p"/api/v1/recordings/#{asciicast.id}", %{"title" => "New Title"})

      assert %{"type" => "unauthenticated", "message" => "Unregistered CLI"} =
               json_response(conn, 401)
    end
  end

  describe "update with unregistered CLI" do
    setup [:register_cli, :authenticate]

    @tag user: [email: nil]
    test "fails", %{conn: conn} do
      asciicast = insert(:asciicast)

      conn = put(conn, ~p"/api/v1/recordings/#{asciicast.id}", %{"title" => "New Title"})

      assert %{"type" => "unauthenticated", "message" => "Unregistered CLI"} =
               json_response(conn, 401)
    end
  end

  describe "update with revoked install ID" do
    setup [:register_cli, :revoke_cli, :authenticate]

    test "fails", %{conn: conn} do
      asciicast = insert(:asciicast)

      conn = put(conn, ~p"/api/v1/recordings/#{asciicast.id}", %{"title" => "New Title"})

      assert %{"type" => "unauthenticated", "message" => "Revoked CLI"} =
               json_response(conn, 401)
    end
  end

  describe "update with registered CLI" do
    setup [:register_cli, :authenticate]

    test "succeeds when attrs are valid", %{conn: conn, cli: cli} do
      asciicast =
        insert(:asciicast,
          user: cli.user,
          title: "Original title",
          description: "Original description"
        )

      conn =
        put(conn, ~p"/api/v1/recordings/#{asciicast.id}", %{
          "title" => "New title",
          "description" => "New description",
          "audio_url" => "https://example.com/audio.opus"
        })

      assert %{
               "id" => id,
               "url" => url,
               "file_url" => file_url,
               "audio_url" => "https://example.com/audio.opus",
               "title" => "New title",
               "description" => "New description",
               "visibility" => "unlisted"
             } = json_response(conn, 200)

      assert is_integer(id)
      assert url =~ @recording_url
      assert file_url =~ ~r/^http.+\.cast$/
    end

    test "succeeds when using secret token", %{conn: conn, cli: cli} do
      _asciicast =
        insert(:asciicast,
          user: cli.user,
          secret_token: "abcdefghijklmnopqrstuvwxy",
          title: "Original title"
        )

      conn =
        put(conn, ~p"/api/v1/recordings/abcdefghijklmnopqrstuvwxy", %{
          "title" => "New title via token"
        })

      assert %{
               "id" => id,
               "url" => url,
               "title" => "New title via token"
             } = json_response(conn, 200)

      assert is_integer(id)
      assert url =~ @recording_url
    end

    test "fails when attrs are not valid", %{conn: conn, cli: cli} do
      asciicast = insert(:asciicast, user: cli.user)

      conn = put(conn, ~p"/api/v1/recordings/#{asciicast.id}", %{"term_cols_override" => 0})

      assert %{
               "type" => "validation_failed",
               "message" => "Validation failed",
               "details" => [
                 %{"field" => "term_cols_override", "message" => "must be greater than 0"}
               ]
             } = json_response(conn, 422)
    end

    test "fails when recording belongs to another user", %{conn: conn} do
      asciicast = insert(:asciicast)
      conn = put(conn, ~p"/api/v1/recordings/#{asciicast.id}", %{"title" => "New Title"})

      assert %{"type" => "access_denied", "message" => "You don't have access to this resource"} =
               json_response(conn, 403)
    end

    test "fails when recording is not found", %{conn: conn} do
      conn = put(conn, ~p"/api/v1/recordings/99999", %{"title" => "New Title"})

      assert %{"type" => "not_found", "message" => "Recording not found"} =
               json_response(conn, 404)
    end
  end

  describe "delete without authentication" do
    test "fails", %{conn: conn} do
      asciicast = insert(:asciicast)

      conn = delete(conn, ~p"/api/v1/recordings/#{asciicast.id}")

      assert %{"type" => "unauthenticated", "message" => "Missing install ID"} =
               json_response(conn, 401)
    end
  end

  describe "delete with unknown install ID" do
    setup [:authenticate]

    test "fails", %{conn: conn} do
      asciicast = insert(:asciicast)

      conn = delete(conn, ~p"/api/v1/recordings/#{asciicast.id}")

      assert %{"type" => "unauthenticated", "message" => "Unregistered CLI"} =
               json_response(conn, 401)
    end
  end

  describe "delete with unregistered CLI" do
    setup [:register_cli, :authenticate]

    @tag user: [email: nil]
    test "fails", %{conn: conn} do
      asciicast = insert(:asciicast)

      conn = delete(conn, ~p"/api/v1/recordings/#{asciicast.id}")

      assert %{"type" => "unauthenticated", "message" => "Unregistered CLI"} =
               json_response(conn, 401)
    end
  end

  describe "delete with revoked install ID" do
    setup [:register_cli, :revoke_cli, :authenticate]

    test "fails", %{conn: conn} do
      asciicast = insert(:asciicast)

      conn = delete(conn, ~p"/api/v1/recordings/#{asciicast.id}")

      assert %{"type" => "unauthenticated", "message" => "Revoked CLI"} =
               json_response(conn, 401)
    end
  end

  describe "delete with registered CLI" do
    setup [:register_cli, :authenticate]

    test "succeeds when deleting own recording", %{conn: conn, cli: cli} do
      asciicast = insert(:asciicast, user: cli.user)

      conn = delete(conn, ~p"/api/v1/recordings/#{asciicast.id}")

      assert response(conn, 204)
    end

    test "succeeds when deleting own recording using secret token", %{conn: conn, cli: cli} do
      _asciicast = insert(:asciicast, user: cli.user, secret_token: "abcdefghijklmnopqrstuvwxy")

      conn = delete(conn, ~p"/api/v1/recordings/abcdefghijklmnopqrstuvwxy")

      assert response(conn, 204)
    end

    test "fails when recording belongs to another user", %{conn: conn} do
      asciicast = insert(:asciicast)
      conn = delete(conn, ~p"/api/v1/recordings/#{asciicast.id}")

      assert %{"type" => "access_denied", "message" => "You don't have access to this resource"} =
               json_response(conn, 403)
    end

    test "fails when recording is not found", %{conn: conn} do
      conn = delete(conn, ~p"/api/v1/recordings/99999")

      assert %{"type" => "not_found", "message" => "Recording not found"} =
               json_response(conn, 404)
    end
  end

  defp upload(conn, upload) do
    conn
    |> put_resp_content_type("application/json")
    |> post(~p"/api/v1/recordings", %{"file" => upload})
  end

  defp require_registered_cli(_context) do
    AppEnv.put(Asciinema.Accounts, upload_auth_required: true)

    :ok
  end

  defp register_cli(%{token: token} = context) do
    user = insert(:user, Map.get(context, :user, []))
    cli = insert(:cli, user: user, token: token)

    [cli: cli]
  end

  defp revoke_cli(%{cli: cli}) do
    [cli: Accounts.revoke_cli!(cli)]
  end

  defp authenticate(%{conn: conn, token: token}) do
    conn =
      if token do
        put_req_header(conn, "authorization", "Basic " <> Base.encode64("test:" <> token))
      else
        conn
      end

    [conn: conn]
  end
end
