defmodule MusicLibrary.RecordsTest do
  use MusicLibrary.DataCase

  import MusicBrainz.Fixtures.Release
  import MusicBrainz.Fixtures.ReleaseGroup
  import MusicLibrary.ColorHelpers, only: [color_hex?: 1]
  import MusicLibrary.Fixtures.Records

  alias MusicLibrary.Assets
  alias MusicLibrary.Records
  alias MusicLibrary.Records.SearchIndex

  defp create_records(_) do
    records = [
      record_with_artist("Marillion", %{title: "Brave", format: :vinyl}),
      record_with_artist("Marillion", %{title: "Brave (Live)", format: :cd, type: :live}),
      record_with_artist("Marillion", %{title: "Afraid of Sunlight"}),
      record_with_artist("Airbag", %{title: "The Greatest Show on Earth"}),
      record_with_artist("Airbag (AU)", %{title: "Libertad"})
    ]

    %{records: records}
  end

  # when searching we do not return all record fields (e.g. cover data)
  # so we rely on record ids to compare results
  defp search(query, limit, offset) do
    SearchIndex
    |> Records.search_records(query, limit: limit, offset: offset, order: :alphabetical)
    |> Enum.map(& &1.id)
  end

  describe "create_record/1" do
    test "populates computed values" do
      record =
        record(musicbrainz_data: release_group(:lockdown_trilogy))

      assert record.release_ids == ["77e746fc-566f-445b-a62b-cc014280fac9"]

      assert record.included_release_group_ids == [
               "749c07b5-4900-404b-bea9-bb6b16fa991e",
               "61077431-0057-4119-8f06-0df1098d21e5",
               "c36123e3-8899-48a5-8196-9dbb72421d69",
               "d463f2b1-d254-4baf-a957-fb78c6e5b956"
             ]

      assert record.cover_hash ==
               "599407DDF69907D4A60FE13CCAA824D25CF08DC124FD6AA3E8E7ECD98C885FFE"

      assert length(record.dominant_colors) == 5
      assert Enum.all?(record.dominant_colors, &color_hex?/1)
    end

    @tag :capture_log
    test "succeeds when color extraction fails" do
      record = record(dominant_colors: [], cover_hash: "nonexistent_hash")

      assert record.dominant_colors == []
    end

    test "queues a task to retrieve artist info data" do
      record =
        record(musicbrainz_data: release_group(:lockdown_trilogy))

      [artist] = record.artists

      assert_enqueued worker: MusicLibrary.Worker.FetchArtistInfo,
                      args: %{id: artist.musicbrainz_id}
    end
  end

  describe "delete_record/1" do
    test "queues a task to delete artist info data" do
      record =
        record(musicbrainz_data: release_group(:lockdown_trilogy))

      [artist] = record.artists

      Records.delete_record(record)

      assert_enqueued worker: MusicLibrary.Worker.PruneArtistInfo,
                      args: %{id: artist.musicbrainz_id}
    end
  end

  describe "refresh_musicbrainz_data/1" do
    test "updates release_ids, included_release_group_ids, and artists" do
      release_group_id = release_group_id(:marbles)

      record =
        record(
          musicbrainz_id: release_group_id,
          musicbrainz_data: Map.put(release_group(:marbles), "releases", [])
        )

      assert record.release_ids == []
      assert record.included_release_group_ids == []

      new_release_group = release_group(:lockdown_trilogy)
      new_release_group_releases = release_group_releases(:lockdown_trilogy)

      Req.Test.stub(MusicBrainz.API, fn conn ->
        case conn.path_info do
          [_ws, _version, "release-group", ^release_group_id] ->
            Req.Test.json(conn, new_release_group)

          [_ws, _version, "release"] ->
            Req.Test.json(conn, new_release_group_releases)
        end
      end)

      {:ok, updated_record} = Records.refresh_musicbrainz_data(record)

      assert record.release_ids !== updated_record.release_ids
      assert record.included_release_group_ids !== updated_record.included_release_group_ids
      assert record.artists !== updated_record.artists
      assert updated_record.artists !== []
    end
  end

  describe "search_records/2" do
    setup [:create_records]

    test "untagged search (with limit and offset)", %{
      records: [brave_vinyl, brave_live_cd | _rest]
    } do
      assert [brave_vinyl.id, brave_live_cd.id] == search("brave", 10, 0)
      assert [brave_vinyl.id] == search("brave", 1, 0)
      assert [brave_live_cd.id] == search("brave", 1, 1)
    end

    test "tagged search - album", %{records: [_brave_vinyl, brave_live_cd | _rest]} do
      assert [brave_live_cd.id] == search(~s(album:"Brave \(Live\)"), 10, 0)
    end

    test "tagged search - artist", %{records: [_, _, _, greatest_show_on_earth, libertad]} do
      assert [greatest_show_on_earth.id, libertad.id] == search("artist:airbag", 10, 0)
      assert [libertad.id] == search(~s(artist:"airbag \(AU\)"), 10, 0)
    end

    test "tagged search - format", %{records: [_brave_vinyl, brave_live_cd | _rest]} do
      assert [brave_live_cd.id] == search("brave format:cd", 10, 0)
    end

    test "tagged search - type", %{records: [_brave_vinyl, brave_live_cd | _rest]} do
      assert [brave_live_cd.id] == search("brave type:live", 10, 0)
    end

    test "tagged search - mbid", %{records: [_, _, _, greatest_show_on_earth, libertad]} do
      [airbag_mbid] = Enum.map(greatest_show_on_earth.artists, fn a -> a.musicbrainz_id end)
      [airbag_au_mbid] = Enum.map(libertad.artists, fn a -> a.musicbrainz_id end)

      assert [greatest_show_on_earth.id] == search("mbid:#{airbag_mbid}", 10, 0)
      assert [libertad.id] == search("mbid:#{airbag_au_mbid}", 10, 0)
    end
  end

  describe "search_records_count/2" do
    setup [:create_records]

    test "untagged search" do
      assert 2 == Records.search_records_count(SearchIndex, "brave")
    end

    test "tagged search - album" do
      assert 1 == Records.search_records_count(SearchIndex, ~s(album:"Brave \(Live\)"))
    end

    test "tagged search - artist" do
      assert 2 == Records.search_records_count(SearchIndex, "artist:airbag")
      assert 1 == Records.search_records_count(SearchIndex, ~s(artist:"airbag \(AU\)"))
    end

    test "tagged search - format" do
      assert 1 == Records.search_records_count(SearchIndex, "brave format:cd")
    end

    test "tagged search - type" do
      assert 1 == Records.search_records_count(SearchIndex, "brave type:live")
    end

    test "tagged search - mbid", %{records: [_, _, _, greatest_show_on_earth, libertad]} do
      [airbag_mbid] = Enum.map(greatest_show_on_earth.artists, fn a -> a.musicbrainz_id end)
      [airbag_au_mbid] = Enum.map(libertad.artists, fn a -> a.musicbrainz_id end)

      assert 1 ==
               Records.search_records_count(SearchIndex, "mbid:#{airbag_mbid}")

      assert 1 == Records.search_records_count(SearchIndex, "mbid:#{airbag_au_mbid}")
    end
  end

  describe "get_record!/1" do
    test "fetches the record by id" do
      # while this test may seem redundant, it implicitely checks that ALL record fields are returned,
      # as opposed to other code paths where we only return essential ones.
      expected = record()

      assert expected == Records.get_record!(expected.id)
    end
  end

  describe "get_artist_records/1" do
    test "returns records with essential data" do
      expected = record()

      artist_musicbrainz_id = expected.artists |> hd() |> Map.get(:musicbrainz_id)

      [artist_record] = Records.get_artist_records(artist_musicbrainz_id)

      assert expected.id == artist_record.id
    end
  end

  describe "import_from_musicbrainz_release_group/2" do
    test "saves a record with its cover art" do
      current_time = DateTime.utc_now()

      release_group = release_group(:marbles)
      release_group_id = release_group_id(:marbles)
      release_group_releases = release_group_releases(:marbles)

      cover_data = marbles_cover_data()

      Req.Test.stub(MusicBrainz.API, fn conn ->
        case conn.path_info do
          [_ws, _version, "release-group", ^release_group_id] ->
            Req.Test.json(conn, release_group)

          [_ws, _version, "release"] ->
            Req.Test.json(conn, release_group_releases)

          [_release_group, ^release_group_id, "front"] ->
            Plug.Conn.send_resp(conn, 200, cover_data)
        end
      end)

      assert {:ok, record} =
               Records.import_from_musicbrainz_release_group(release_group_id,
                 format: :vinyl,
                 purchased_at: current_time
               )

      assert [artist] = record.artists
      assert artist.name == "Marillion"

      assert record.musicbrainz_id == release_group_id
      assert record.title == "Marbles"
      assert record.format == :vinyl
      assert record.purchased_at == DateTime.truncate(current_time, :second)

      assert record.release_ids ==
               [
                 "0e290154-5375-4f4f-a658-4a92bf02faa5",
                 "3f1cc80f-4507-48a9-899c-c1bda83280c2",
                 "d3f9b9e2-73f5-4b47-a2a7-2c2199aad608",
                 "2c4ecd84-7a84-4f42-a600-2f00ed8978c9",
                 "ab151aa6-7538-4e93-be60-eded52b5b7b7",
                 "b94bbd1f-ae5d-4e7b-98ff-28bfe135f20c",
                 "4b9fe13b-4837-4c02-9368-e97ba6f5a086",
                 "3f89357a-eeb3-4040-af34-a27b7c2aea2b",
                 "a4b02377-0b5e-448e-9cd6-5500c0378523",
                 "f3937bc5-b99f-443a-9609-a404201f21ca"
               ]
    end
  end

  describe "import_from_musicbrainz_release/2" do
    test "saves a record with its cover art" do
      current_time = DateTime.utc_now()

      release = release(:marbles)
      release_id = release_id(:marbles)

      release_group = release_group(:marbles)
      release_group_id = release_group_id(:marbles)
      release_group_releases = release_group_releases(:marbles)

      cover_data = marbles_cover_data()

      Req.Test.stub(MusicBrainz.API, fn conn ->
        case conn.path_info do
          [_ws, _version, "release-group", ^release_group_id] ->
            Req.Test.json(conn, release_group)

          [_ws, _version, "release", ^release_id] ->
            Req.Test.json(conn, release)

          [_ws, _version, "release"] ->
            Req.Test.json(conn, release_group_releases)

          [_release_group, ^release_group_id, "front"] ->
            Plug.Conn.send_resp(conn, 200, cover_data)
        end
      end)

      assert {:ok, record} =
               Records.import_from_musicbrainz_release(release_id,
                 format: :vinyl,
                 purchased_at: current_time
               )

      assert [artist] = record.artists
      assert artist.name == "Marillion"

      assert record.musicbrainz_id == release_group_id
      assert record.title == "Marbles"
      assert record.format == :vinyl
      assert record.purchased_at == DateTime.truncate(current_time, :second)

      assert record.release_ids ==
               [
                 "0e290154-5375-4f4f-a658-4a92bf02faa5",
                 "3f1cc80f-4507-48a9-899c-c1bda83280c2",
                 "d3f9b9e2-73f5-4b47-a2a7-2c2199aad608",
                 "2c4ecd84-7a84-4f42-a600-2f00ed8978c9",
                 "ab151aa6-7538-4e93-be60-eded52b5b7b7",
                 "b94bbd1f-ae5d-4e7b-98ff-28bfe135f20c",
                 "4b9fe13b-4837-4c02-9368-e97ba6f5a086",
                 "3f89357a-eeb3-4040-af34-a27b7c2aea2b",
                 "a4b02377-0b5e-448e-9cd6-5500c0378523",
                 "f3937bc5-b99f-443a-9609-a404201f21ca"
               ]
    end
  end

  describe "refresh_cover/1" do
    test "fetches and stores the updated cover" do
      record = record(cover_data: marbles_cover_data())

      raven_cover_data = raven_cover_data()

      Req.Test.stub(MusicBrainz.API, fn conn ->
        Plug.Conn.send_resp(conn, 200, raven_cover_data)
      end)

      assert {:ok, updated_record} = Records.refresh_cover(record)

      assert updated_record.cover_hash ==
               "6E0D25D1FD1019D771D7EB3F777E2C7C1B06A73A92E56A584D674D86DD8AF441"

      {:ok, expected_content} = Assets.Image.resize(raven_cover_data())

      assert Assets.get(updated_record.cover_hash).content == expected_content
    end
  end

  describe "populate_genres/1" do
    test "updates record genres from OpenAI response" do
      record = record(%{genres: []})
      genres = ["progressive rock", "art rock", "symphonic rock"]

      Req.Test.stub(OpenAI.API, fn conn ->
        Req.Test.json(conn, %{
          "choices" => [
            %{"message" => %{"content" => JSON.encode!(%{"genres" => genres})}}
          ]
        })
      end)

      assert {:ok, updated} = Records.populate_genres(record)
      assert updated.genres == genres
    end

    @tag :capture_log
    test "returns error tuple when OpenAI API fails" do
      record = record(%{genres: []})

      Req.Test.stub(OpenAI.API, fn conn ->
        Plug.Conn.send_resp(conn, 500, JSON.encode!(%{"error" => "internal server error"}))
      end)

      assert {:error, _reason} = Records.populate_genres(record)
    end
  end
end
