defmodule Mobilizon.Federation.ActivityPub.ActorTest do
  use Mobilizon.DataCase
  import Mox
  import Mock

  alias Mobilizon.Actors
  alias Mobilizon.Actors.Actor

  alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
  alias Mobilizon.Federation.ActivityPub.Relay
  alias Mobilizon.Service.HTTP.ActivityPub.Mock
  alias Mobilizon.Service.HTTP.HostMetaClient.Mock, as: HostMetaClientMock
  alias Mobilizon.Service.HTTP.WebfingerClient.Mock, as: WebfingerClientMock

  describe "fetching actor from its url" do
    @actor_url "https://framapiaf.org/users/tcit"
    test "returns an actor from nickname" do
      actor_data =
        File.read!("test/fixtures/mastodon-actor.json")
        |> Jason.decode!()
        |> Map.put("id", @actor_url)
        |> Map.put("preferredUsername", "tcit")
        |> Map.put("discoverable", true)

      Mock
      |> expect(:call, fn
        %{method: :get, url: @actor_url}, _opts ->
          {:ok, %Tesla.Env{status: 200, body: actor_data}}
      end)

      HostMetaClientMock
      |> expect(:call, fn
        %{method: :get, url: "https://framapiaf.org/.well-known/host-meta"}, _opts ->
          {:ok, %Tesla.Env{status: 404, body: ""}}
      end)

      webfinger_data =
        File.read!("test/fixtures/webfinger/mastodon-webfinger.json")
        |> String.replace("social.tcit.fr", "framapiaf.org")
        |> Jason.decode!()

      WebfingerClientMock
      |> expect(:call, fn
        %{
          method: :get,
          url: "https://framapiaf.org/.well-known/webfinger?resource=acct:tcit@framapiaf.org"
        },
        _opts ->
          {:ok, %Tesla.Env{status: 200, body: webfinger_data}}
      end)

      assert {:ok,
              %Actor{preferred_username: "tcit", domain: "framapiaf.org", visibility: :public} =
                _actor} = ActivityPubActor.make_actor_from_nickname("tcit@framapiaf.org")
    end

    test "returns an actor from nickname when not discoverable" do
      actor_data =
        File.read!("test/fixtures/mastodon-actor.json")
        |> Jason.decode!()
        |> Map.put("id", @actor_url)
        |> Map.put("preferredUsername", "tcit")

      Mock
      |> expect(:call, fn
        %{method: :get, url: @actor_url}, _opts ->
          {:ok, %Tesla.Env{status: 200, body: actor_data}}
      end)

      HostMetaClientMock
      |> expect(:call, fn
        %{method: :get, url: "https://framapiaf.org/.well-known/host-meta"}, _opts ->
          {:ok, %Tesla.Env{status: 404, body: ""}}
      end)

      webfinger_data =
        File.read!("test/fixtures/webfinger/mastodon-webfinger.json")
        |> String.replace("social.tcit.fr", "framapiaf.org")
        |> Jason.decode!()

      WebfingerClientMock
      |> expect(:call, fn
        %{
          method: :get,
          url: "https://framapiaf.org/.well-known/webfinger?resource=acct:tcit@framapiaf.org"
        },
        _opts ->
          {:ok, %Tesla.Env{status: 200, body: webfinger_data}}
      end)

      assert {:ok,
              %Actor{preferred_username: "tcit", domain: "framapiaf.org", visibility: :unlisted} =
                _actor} = ActivityPubActor.make_actor_from_nickname("tcit@framapiaf.org")
    end

    test "returns an actor from url" do
      actor_data =
        File.read!("test/fixtures/mastodon-actor.json")
        |> Jason.decode!()
        |> Map.put("id", @actor_url)
        |> Map.put("preferredUsername", "tcit")

      Mock
      |> expect(:call, fn
        %{method: :get, url: @actor_url}, _opts ->
          {:ok, %Tesla.Env{status: 200, body: actor_data}}
      end)

      # Initial fetch
      # Unlisted because discoverable is not present in the JSON payload
      assert {:ok,
              %Actor{preferred_username: "tcit", domain: "framapiaf.org", visibility: :unlisted}} =
               ActivityPubActor.get_or_fetch_actor_by_url(@actor_url)

      # Fetch uses cache if Actors.needs_update? returns false
      with_mocks([
        {Actors, [:passthrough],
         [
           get_actor_by_url: fn @actor_url, false ->
             {:ok,
              %Actor{
                preferred_username: "tcit",
                domain: "framapiaf.org"
              }}
           end,
           needs_update?: fn _ -> false end
         ]},
        {ActivityPubActor, [:passthrough],
         make_actor_from_url: fn @actor_url, [] ->
           {:ok,
            %Actor{
              preferred_username: "tcit",
              domain: "framapiaf.org"
            }}
         end}
      ]) do
        assert {:ok, %Actor{preferred_username: "tcit", domain: "framapiaf.org"}} =
                 ActivityPubActor.get_or_fetch_actor_by_url(@actor_url)

        assert_called(Actors.needs_update?(:_))
        refute called(ActivityPubActor.make_actor_from_url(@actor_url, []))
      end

      # Fetch doesn't use cache if Actors.needs_update? returns true
      with_mocks([
        {Actors, [:passthrough],
         [
           get_actor_by_url: fn @actor_url, false ->
             {:ok,
              %Actor{
                preferred_username: "tcit",
                domain: "framapiaf.org"
              }}
           end,
           needs_update?: fn _ -> true end
         ]},
        {ActivityPubActor, [:passthrough],
         make_actor_from_url: fn @actor_url, [] ->
           {:ok,
            %Actor{
              preferred_username: "tcit",
              domain: "framapiaf.org"
            }}
         end}
      ]) do
        assert {:ok, %Actor{preferred_username: "tcit", domain: "framapiaf.org"}} =
                 ActivityPubActor.get_or_fetch_actor_by_url(@actor_url)

        assert_called(ActivityPubActor.get_or_fetch_actor_by_url(@actor_url))
        assert_called(Actors.get_actor_by_url(@actor_url, false))
        assert_called(Actors.needs_update?(:_))
        assert_called(ActivityPubActor.make_actor_from_url(@actor_url, []))
      end
    end

    test "handles remote actor being deleted" do
      Mock
      |> expect(:call, fn
        %{method: :get, url: @actor_url}, _opts ->
          {:ok, %Tesla.Env{status: 410, body: ""}}
      end)

      assert match?(
               {:error, :actor_deleted},
               ActivityPubActor.make_actor_from_url(@actor_url, [])
             )
    end

    @public_url "https://www.w3.org/ns/activitystreams#Public"
    test "activitystreams#Public uri returns Relay actor" do
      assert ActivityPubActor.get_or_fetch_actor_by_url(@public_url) == {:ok, Relay.get_actor()}
    end
  end
end