defmodule MusicLibrary.ScrobbleActivityTest do
  @moduledoc false

  use MusicLibrary.DataCase

  alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures
  alias MusicBrainz.Release
  alias MusicBrainz.Release.{Artist, Medium, Track}
  alias MusicLibrary.ScrobbleActivity
  alias MusicLibrary.Secrets

  @release ReleaseFixtures.release_with_media(:marbles) |> Release.from_api_response()

  defp store_session_key(_) do
    Secrets.store("last_fm_session_key", "test_session_key")
    :ok
  end

  defp stub_lastfm_success(_) do
    Req.Test.stub(LastFm.API, fn conn ->
      Req.Test.json(conn, %{"scrobbles" => %{"@attr" => %{"accepted" => 1}}})
    end)

    :ok
  end

  describe "can_scrobble?/0" do
    test "returns false when no session key is stored" do
      refute ScrobbleActivity.can_scrobble?()
    end

    test "returns true when session key exists" do
      Secrets.store("last_fm_session_key", "test_session_key")
      assert ScrobbleActivity.can_scrobble?()
    end
  end

  describe "scrobble_release/3" do
    setup [:store_session_key, :stub_lastfm_success]

    test "scrobbles all tracks with :started_at" do
      started_at = DateTime.utc_now()
      assert {:ok, _} = ScrobbleActivity.scrobble_release(@release, :started_at, started_at)
    end

    test "scrobbles all tracks with :finished_at" do
      finished_at = DateTime.utc_now()
      assert {:ok, _} = ScrobbleActivity.scrobble_release(@release, :finished_at, finished_at)
    end

    test "returns error when release has zero duration" do
      zero_duration_release = %{@release | media: [%{tracks: [%{length: 0}]}]}
      started_at = DateTime.utc_now()

      assert {:error, :no_duration} =
               ScrobbleActivity.scrobble_release(zero_duration_release, :started_at, started_at)
    end
  end

  describe "all scrobble operations without session key" do
    setup [:stub_lastfm_success]

    test "scrobble_release/3 returns :no_session_key" do
      assert {:error, :no_session_key} =
               ScrobbleActivity.scrobble_release(@release, :started_at, DateTime.utc_now())
    end

    test "scrobble_medium/4 returns :no_session_key" do
      assert {:error, :no_session_key} =
               ScrobbleActivity.scrobble_medium(1, @release, :started_at, DateTime.utc_now())
    end

    test "scrobble_tracks/4 returns :no_session_key" do
      track_ids =
        @release
        |> Release.tracks()
        |> Enum.take(1)
        |> Enum.map(& &1.id)
        |> MapSet.new()

      assert {:error, :no_session_key} =
               ScrobbleActivity.scrobble_tracks(
                 track_ids,
                 @release,
                 :started_at,
                 DateTime.utc_now()
               )
    end
  end

  describe "scrobble_medium/4" do
    setup [:store_session_key, :stub_lastfm_success]

    test "scrobbles tracks from a specific medium with :started_at" do
      started_at = DateTime.utc_now()
      assert {:ok, _} = ScrobbleActivity.scrobble_medium(1, @release, :started_at, started_at)
    end

    test "scrobbles tracks from a specific medium with :finished_at" do
      finished_at = DateTime.utc_now()
      assert {:ok, _} = ScrobbleActivity.scrobble_medium(1, @release, :finished_at, finished_at)
    end

    test "returns error when medium not found" do
      started_at = DateTime.utc_now()

      assert {:error, :medium_not_found} =
               ScrobbleActivity.scrobble_medium(99, @release, :started_at, started_at)
    end

    test "returns error when medium not found with :finished_at" do
      finished_at = DateTime.utc_now()

      assert {:error, :medium_not_found} =
               ScrobbleActivity.scrobble_medium(99, @release, :finished_at, finished_at)
    end

    test "returns error when medium has zero duration" do
      zero_duration_medium = %Medium{
        title: "",
        format: "CD",
        number: 1,
        track_count: 1,
        tracks: [
          %Track{id: "t1", title: "Silent", length: 0, artists: [], number: "1", position: 1}
        ]
      }

      release = %{@release | media: [zero_duration_medium]}
      started_at = DateTime.utc_now()

      assert {:error, :no_duration} =
               ScrobbleActivity.scrobble_medium(1, release, :started_at, started_at)
    end
  end

  describe "scrobble_tracks/4" do
    setup [:store_session_key, :stub_lastfm_success]

    setup do
      track_ids =
        @release
        |> Release.tracks()
        |> Enum.take(2)
        |> Enum.map(& &1.id)
        |> MapSet.new()

      %{track_ids: track_ids}
    end

    test "scrobbles selected tracks with :started_at", %{track_ids: track_ids} do
      started_at = DateTime.utc_now()

      assert {:ok, _} =
               ScrobbleActivity.scrobble_tracks(track_ids, @release, :started_at, started_at)
    end

    test "scrobbles selected tracks with :finished_at", %{track_ids: track_ids} do
      finished_at = DateTime.utc_now()

      assert {:ok, _} =
               ScrobbleActivity.scrobble_tracks(track_ids, @release, :finished_at, finished_at)
    end

    test "returns error when no tracks selected" do
      started_at = DateTime.utc_now()

      assert {:error, :no_duration} =
               ScrobbleActivity.scrobble_tracks(MapSet.new(), @release, :started_at, started_at)
    end

    test "returns error when selected tracks have zero length" do
      zero_track = %Track{
        id: "zero",
        title: "Silent",
        length: 0,
        artists: [],
        number: "1",
        position: 1
      }

      release = %{
        @release
        | media: [
            %Medium{title: "", format: "CD", number: 1, track_count: 1, tracks: [zero_track]}
          ]
      }

      started_at = DateTime.utc_now()

      assert {:error, :no_duration} =
               ScrobbleActivity.scrobble_tracks(
                 MapSet.new(["zero"]),
                 release,
                 :started_at,
                 started_at
               )
    end
  end

  describe "scrobble construction" do
    setup [:store_session_key]

    test "populates correct fields and increments timestamps" do
      started_at = ~U[2024-01-01 12:00:00Z]

      Req.Test.stub(LastFm.API, fn conn ->
        {:ok, body, conn} = Plug.Conn.read_body(conn)
        params = URI.decode_query(body)

        # First track should have timestamp = started_at + track length
        first_track = Enum.at(Release.tracks(@release), 0)
        expected_timestamp = DateTime.add(started_at, first_track.length, :millisecond)

        assert params["track[0]"] == first_track.title
        assert params["artist[0]"] == first_track.artists |> List.first() |> Map.get(:name)
        assert params["album[0]"] == @release.title
        assert params["timestamp[0]"] == Integer.to_string(DateTime.to_unix(expected_timestamp))

        Req.Test.json(conn, %{"scrobbles" => %{"@attr" => %{"accepted" => 1}}})
      end)

      assert {:ok, _} = ScrobbleActivity.scrobble_release(@release, :started_at, started_at)
    end

    test "sets album_artist when track artist differs from release artist" do
      different_artist = %Artist{id: "a2", name: "Guest Artist", sort_name: "Guest"}

      track = %Track{
        id: "t1",
        title: "Collab",
        length: 60_000,
        artists: [different_artist],
        number: "1",
        position: 1
      }

      medium = %Medium{title: "", format: "CD", number: 1, track_count: 1, tracks: [track]}
      release = %{@release | media: [medium]}

      Req.Test.stub(LastFm.API, fn conn ->
        {:ok, body, conn} = Plug.Conn.read_body(conn)
        params = URI.decode_query(body)

        release_artist = @release.artists |> List.first() |> Map.get(:name)
        assert params["album_artist[0]"] == release_artist

        Req.Test.json(conn, %{"scrobbles" => %{"@attr" => %{"accepted" => 1}}})
      end)

      assert {:ok, _} =
               ScrobbleActivity.scrobble_release(release, :started_at, ~U[2024-01-01 12:00:00Z])
    end

    test "does not set album_artist when track artist matches release artist" do
      track_ids =
        @release
        |> Release.tracks()
        |> Enum.take(1)
        |> Enum.map(& &1.id)
        |> MapSet.new()

      Req.Test.stub(LastFm.API, fn conn ->
        {:ok, body, conn} = Plug.Conn.read_body(conn)
        params = URI.decode_query(body)

        refute Map.has_key?(params, "album_artist[0]")

        Req.Test.json(conn, %{"scrobbles" => %{"@attr" => %{"accepted" => 1}}})
      end)

      assert {:ok, _} =
               ScrobbleActivity.scrobble_tracks(
                 track_ids,
                 @release,
                 :started_at,
                 ~U[2024-01-01 12:00:00Z]
               )
    end
  end
end
