defmodule MusicLibrary.ListeningStatsTest do
  use MusicLibrary.DataCase

  import MusicLibrary.ArtistInfoFixtures
  import MusicLibrary.ScrobbledTracksFixtures

  alias LastFm.Track
  alias MusicLibrary.Fixtures.Records, as: RecordsFixtures
  alias MusicLibrary.ListeningStats
  alias MusicLibrary.Records.Record

  describe "update/1 and subscribe/0" do
    test "stores tracks and broadcasts the updated track count" do
      :ok = ListeningStats.subscribe()

      track_one = %Track{
        musicbrainz_id: "5689211e-9afa-3c3e-8e34-63dc0de45ef1",
        title: "The Flow",
        artist: %LastFm.Artist{
          musicbrainz_id: "0cf0af1f-20ca-4863-9b24-5f52772f7715",
          name: "Anekdoten"
        },
        album: %LastFm.Album{
          musicbrainz_id: "08237599-8fdf-4e2b-a7c9-eb5336f60346",
          title: "Vemod"
        },
        cover_url:
          "https://lastfm.freetls.fastly.net/i/u/64s/9741e297b9884a4294624f0f90e14749.jpg",
        scrobbled_at_uts: 1_731_318_211,
        scrobbled_at_label: "11 Nov 2024, 09:43",
        last_fm_data: %{}
      }

      track_two = %Track{
        musicbrainz_id: "619cb295-b155-3e35-b65a-396a7cd1fc47",
        title: "Wheel",
        artist: %LastFm.Artist{
          musicbrainz_id: "0cf0af1f-20ca-4863-9b24-5f52772f7715",
          name: "Anekdoten"
        },
        album: %LastFm.Album{
          musicbrainz_id: "08237599-8fdf-4e2b-a7c9-eb5336f60346",
          title: "Vemod"
        },
        cover_url:
          "https://lastfm.freetls.fastly.net/i/u/64s/9741e297b9884a4294624f0f90e14749.jpg",
        scrobbled_at_uts: 1_731_318_945,
        scrobbled_at_label: "11 Nov 2024, 09:55",
        last_fm_data: %{}
      }

      assert {:ok, 2} == ListeningStats.update([track_two, track_one])
      assert_receive %{track_count: 2}

      # Tracks have already been inserted, count of new tracks is 0
      assert {:ok, 0} == ListeningStats.update([track_two, track_one])
      assert_receive %{track_count: 0}
    end
  end

  describe "scrobble_count/0" do
    test "returns correct count" do
      initial_count = ListeningStats.scrobble_count()

      create_test_tracks(3)

      new_count = ListeningStats.scrobble_count()

      assert new_count == initial_count + 3
    end
  end

  describe "list_tracks/1" do
    test "returns all tracks when no parameters provided" do
      _track1 = track_fixture(%{title: "First Track"})
      _track2 = track_fixture(%{title: "Second Track"})

      tracks = list_tracks()

      assert length(tracks) == 2
      track_titles = Enum.map(tracks, & &1.title)
      assert "First Track" in track_titles
      assert "Second Track" in track_titles
    end

    test "returns tracks ordered by scrobbled_at_uts by default" do
      _older_track =
        track_fixture(%{
          title: "Older Track",
          scrobbled_at_uts: System.system_time(:second) - 3600
        })

      _newer_track =
        track_fixture(%{
          title: "Newer Track",
          scrobbled_at_uts: System.system_time(:second)
        })

      tracks = list_tracks(%{order: :scrobbled_at})

      assert length(tracks) == 2
      # Should be ordered by scrobbled_at_uts descending (newest first)
      assert List.first(tracks).title == "Newer Track"
      assert List.last(tracks).title == "Older Track"
    end

    test "returns tracks ordered by title" do
      track_fixture(%{title: "Zebra Track"})
      track_fixture(%{title: "Alpha Track"})

      tracks = list_tracks(%{order: :title})

      assert length(tracks) == 2
      assert List.first(tracks).title == "Alpha Track"
      assert List.last(tracks).title == "Zebra Track"
    end

    test "returns tracks ordered by artist name" do
      track_fixture(%{artist_name: "Zebra Artist", title: "Track 1"})
      track_fixture(%{artist_name: "Alpha Artist", title: "Track 2"})

      tracks = list_tracks(%{order: :artist})

      assert length(tracks) == 2
      assert List.first(tracks).artist.name == "Alpha Artist"
      assert List.last(tracks).artist.name == "Zebra Artist"
    end

    test "returns tracks ordered by album title" do
      track_fixture(%{album_title: "Zebra Album", title: "Track 1"})
      track_fixture(%{album_title: "Alpha Album", title: "Track 2"})

      tracks = list_tracks(%{order: :album})

      assert length(tracks) == 2
      assert List.first(tracks).album.title == "Alpha Album"
      assert List.last(tracks).album.title == "Zebra Album"
    end

    test "filters tracks by search query matching track title" do
      track_fixture(%{title: "Special Track"})
      track_fixture(%{title: "Regular Track"})

      tracks = list_tracks(%{query: "Special"})

      assert length(tracks) == 1
      assert List.first(tracks).title == "Special Track"
    end

    test "filters tracks by search query matching artist name" do
      track_fixture(%{artist_name: "Special Artist", title: "Track 1"})
      track_fixture(%{artist_name: "Regular Artist", title: "Track 2"})

      tracks = list_tracks(%{query: "Special Artist"})

      assert length(tracks) == 1
      assert List.first(tracks).artist.name == "Special Artist"
    end

    test "filters tracks by search query matching album title" do
      track_fixture(%{album_title: "Special Album", title: "Track 1"})
      track_fixture(%{album_title: "Regular Album", title: "Track 2"})

      tracks = list_tracks(%{query: "Special Album"})

      assert length(tracks) == 1
      assert List.first(tracks).album.title == "Special Album"
    end

    test "applies pagination correctly" do
      create_test_tracks(5)

      # Get first 2 tracks
      tracks_page_1 = list_tracks(%{page: 1, page_size: 2})
      assert length(tracks_page_1) == 2

      # Get next 2 tracks
      tracks_page_2 = list_tracks(%{page: 2, page_size: 2})
      assert length(tracks_page_2) == 2

      # Ensure they're different tracks
      page_1_ids = Enum.map(tracks_page_1, & &1.scrobbled_at_uts)
      page_2_ids = Enum.map(tracks_page_2, & &1.scrobbled_at_uts)
      assert Enum.empty?(page_1_ids -- (page_1_ids -- page_2_ids))
    end

    test "returns empty list when query matches no tracks" do
      track_fixture(%{title: "Test Track"})

      tracks = list_tracks(%{query: "NonexistentTrack"})

      assert tracks == []
    end
  end

  describe "get_track!/1" do
    test "returns the track with given scrobbled_at_uts as integer" do
      track = track_fixture()

      found_track = ListeningStats.get_track!(track.scrobbled_at_uts)

      assert found_track.scrobbled_at_uts == track.scrobbled_at_uts
      assert found_track.title == track.title
    end

    test "returns the track with given scrobbled_at_uts as string" do
      track = track_fixture()

      found_track = ListeningStats.get_track!(to_string(track.scrobbled_at_uts))

      assert found_track.scrobbled_at_uts == track.scrobbled_at_uts
    end

    test "raises Ecto.NoResultsError when track does not exist" do
      assert_raise Ecto.NoResultsError, fn ->
        ListeningStats.get_track!(999_999_999)
      end
    end

    test "raises Ecto.NoResultsError when given invalid string" do
      assert_raise Ecto.NoResultsError, fn ->
        ListeningStats.get_track!("invalid")
      end
    end
  end

  describe "update_track/2" do
    test "updates the track with valid attributes" do
      track = track_fixture(%{title: "Original Title"})

      update_attrs = %{
        title: "Updated Title",
        artist: %{name: "Updated Artist"},
        album: %{title: "Updated Album"}
      }

      assert {:ok, updated_track} = ListeningStats.update_track(track, update_attrs)
      assert updated_track.title == "Updated Title"
      assert updated_track.artist.name == "Updated Artist"
      assert updated_track.album.title == "Updated Album"
    end

    test "returns error changeset with invalid attributes" do
      track = track_fixture()

      invalid_attrs = %{title: ""}

      assert {:error, %Ecto.Changeset{}} = ListeningStats.update_track(track, invalid_attrs)
    end

    test "updates scrobbled_at_label" do
      track = track_fixture(%{scrobbled_at_label: "01/01/2024 12:00:00"})

      update_attrs = %{scrobbled_at_label: "02/02/2024 14:30:00"}

      assert {:ok, updated_track} = ListeningStats.update_track(track, update_attrs)
      assert updated_track.scrobbled_at_label == "02/02/2024 14:30:00"
    end

    test "updates cover_url" do
      track = track_fixture(%{cover_url: "https://example.com/old.jpg"})

      update_attrs = %{cover_url: "https://example.com/new.jpg"}

      assert {:ok, updated_track} = ListeningStats.update_track(track, update_attrs)
      assert updated_track.cover_url == "https://example.com/new.jpg"
    end
  end

  describe "delete_track/1" do
    test "deletes the track" do
      track = track_fixture()

      assert {:ok, %Track{}} = ListeningStats.delete_track(track)

      assert_raise Ecto.NoResultsError, fn ->
        ListeningStats.get_track!(track.scrobbled_at_uts)
      end
    end

    test "raises when track has already been deleted" do
      track = track_fixture()
      {:ok, _} = ListeningStats.delete_track(track)

      # Attempt to delete again should fail
      assert_raise Ecto.StaleEntryError, fn ->
        ListeningStats.delete_track(track)
      end
    end
  end

  describe "search_tracks_count/1" do
    test "returns total count when no query provided" do
      create_test_tracks(3)

      count = ListeningStats.search_tracks_count()

      assert count == 3
    end

    test "returns filtered count when query provided" do
      track_fixture(%{title: "Special Track"})
      track_fixture(%{title: "Regular Track"})
      track_fixture(%{title: "Another Track"})

      count = ListeningStats.search_tracks_count("Special")

      assert count == 1
    end

    test "returns zero when query matches no tracks" do
      track_fixture(%{title: "Test Track"})

      count = ListeningStats.search_tracks_count("Nonexistent")

      assert count == 0
    end

    test "counts tracks matching artist name" do
      track_fixture(%{artist_name: "Special Artist", title: "Track 1"})
      track_fixture(%{artist_name: "Regular Artist", title: "Track 2"})

      count = ListeningStats.search_tracks_count("Special")

      assert count == 1
    end

    test "counts tracks matching album title" do
      track_fixture(%{album_title: "Special Album", title: "Track 1"})
      track_fixture(%{album_title: "Regular Album", title: "Track 2"})

      count = ListeningStats.search_tracks_count("Special")

      assert count == 1
    end
  end

  describe "deduplication when multiple records share release IDs" do
    setup do
      # Create two collected records that share the same MusicBrainz release IDs
      # (both use the default marbles fixture with identical musicbrainz_data).
      # This produces two rows per release_id in record_releases, which previously
      # caused fan-out in ListeningStats joins.
      record_a = RecordsFixtures.record(%{title: "Marbles Copy A"})
      record_b = RecordsFixtures.record(%{title: "Marbles Copy B"})

      # Pick a release_id shared by both records
      shared_release_id = "d3f9b9e2-73f5-4b47-a2a7-2c2199aad608"
      now = System.system_time(:second)

      track_fixture(%{
        title: "The Invisible Man",
        album_title: "Marbles",
        album_musicbrainz_id: shared_release_id,
        artist_name: "Marillion",
        scrobbled_at_uts: now - 100
      })

      track_fixture(%{
        title: "You're Gone",
        album_title: "Marbles",
        album_musicbrainz_id: shared_release_id,
        artist_name: "Marillion",
        scrobbled_at_uts: now - 200
      })

      # The lexicographically smaller record_id should consistently win (MIN)
      expected_record_id = Enum.min([record_a.id, record_b.id])

      %{
        expected_record_id: expected_record_id,
        shared_release_id: shared_release_id
      }
    end

    test "list_tracks returns one row per track", %{expected_record_id: expected_record_id} do
      tracks = ListeningStats.list_tracks()

      titles = Enum.map(tracks, & &1.track.title)
      assert length(titles) == length(Enum.uniq(titles)), "list_tracks returned duplicate rows"

      # Both tracks should map to the same deterministic record_id
      for result <- tracks do
        assert result.collected_record_id == expected_record_id
      end
    end

    test "recent_activity returns one row per track", %{expected_record_id: expected_record_id} do
      %{recent_tracks: recent_tracks} =
        ListeningStats.recent_activity("Etc/UTC", 20)

      titles = Enum.map(recent_tracks, & &1.track.title)

      assert length(titles) == length(Enum.uniq(titles)),
             "recent_activity returned duplicate rows"

      for result <- recent_tracks do
        assert result.collected_record_id == expected_record_id
      end
    end

    test "get_top_albums returns one entry per album", %{expected_record_id: expected_record_id} do
      results = ListeningStats.get_top_albums(limit: 10)

      marbles_entries = Enum.filter(results, fn r -> r.album_title == "Marbles" end)
      assert length(marbles_entries) == 1, "get_top_albums returned duplicate album entries"

      [entry] = marbles_entries
      assert entry.play_count == 2
      assert entry.collected_record_id == expected_record_id
    end
  end

  describe "get_top_artists/1" do
    test "counts tracks with missing artist_infos records" do
      artist_info =
        artist_info_fixture(%{musicbrainz_data: %{"name" => "Thin Lizzy"}})

      artist_mbid = artist_info.id

      now = System.system_time(:second)

      # Two tracks with a matching artist_infos record
      track_fixture(%{
        artist_name: "Thin Lizzy",
        artist_musicbrainz_id: artist_mbid,
        title: "The Boys Are Back in Town",
        scrobbled_at_uts: now - 100
      })

      track_fixture(%{
        artist_name: "Thin Lizzy",
        artist_musicbrainz_id: artist_mbid,
        title: "Jailbreak",
        scrobbled_at_uts: now - 200
      })

      # One track with an empty musicbrainz_id (no matching artist_infos)
      track_fixture(%{
        artist_name: "Thin Lizzy",
        artist_musicbrainz_id: "",
        title: "Rosalie / Cowgirl's Song",
        scrobbled_at_uts: now - 300
      })

      results = ListeningStats.get_top_artists(limit: 10)

      # All 3 tracks should be counted as a single artist entry
      assert [thin_lizzy] = Enum.filter(results, fn r -> r.name == "Thin Lizzy" end)
      assert thin_lizzy.play_count == 3
      # MAX picks the real musicbrainz_id over the empty string
      assert thin_lizzy.musicbrainz_id == artist_mbid
    end

    test "returns image_hash from artist_infos when available" do
      artist_info =
        artist_info_fixture(%{
          musicbrainz_data: %{"name" => "Test Artist"},
          image_data_hash: "abc123"
        })

      artist_mbid = artist_info.id

      track_fixture(%{
        artist_name: "Test Artist",
        artist_musicbrainz_id: artist_mbid,
        title: "Track 1",
        scrobbled_at_uts: System.system_time(:second) - 100
      })

      [result] = ListeningStats.get_top_artists(limit: 10)

      assert result.image_hash == "abc123"
    end

    test "returns nil image_hash for tracks without artist_infos" do
      track_fixture(%{
        artist_name: "Unknown Artist",
        artist_musicbrainz_id: "",
        title: "Track 1",
        scrobbled_at_uts: System.system_time(:second) - 100
      })

      [result] = ListeningStats.get_top_artists(limit: 10)

      assert result.image_hash == nil
    end
  end

  describe "get_last_listened_track/1" do
    test "returns the most recent scrobbled track for a record" do
      record = RecordsFixtures.record()
      main_artist = Record.main_artist(record)
      now = System.system_time(:second)

      _older =
        track_fixture(%{
          artist_name: main_artist.name,
          album_title: record.title,
          title: "Older Track",
          scrobbled_at_uts: now - 200
        })

      track_fixture(%{
        artist_name: main_artist.name,
        album_title: record.title,
        title: "Newer Track",
        scrobbled_at_uts: now - 100
      })

      result = ListeningStats.get_last_listened_track(record)

      assert result.title == "Newer Track"
      assert result.scrobbled_at_uts == now - 100
    end

    test "returns nil when no scrobbles exist for the record" do
      record = RecordsFixtures.record()

      assert ListeningStats.get_last_listened_track(record) == nil
    end
  end

  describe "play_count/1" do
    test "returns the correct scrobble count for a record" do
      record = RecordsFixtures.record()
      main_artist = Record.main_artist(record)
      now = System.system_time(:second)

      for i <- 1..3 do
        track_fixture(%{
          artist_name: main_artist.name,
          album_title: record.title,
          title: "Track #{i}",
          scrobbled_at_uts: now - i * 100
        })
      end

      assert ListeningStats.play_count(record) == 3
    end

    test "returns 0 when no scrobbles exist for the record" do
      record = RecordsFixtures.record()

      assert ListeningStats.play_count(record) == 0
    end
  end

  describe "get_top_artists_by_days/2" do
    test "counts tracks with missing artist_infos records within date range" do
      artist_info =
        artist_info_fixture(%{musicbrainz_data: %{"name" => "Thin Lizzy"}})

      artist_mbid = artist_info.id

      now = DateTime.utc_now()
      now_unix = DateTime.to_unix(now)

      # Two tracks with matching artist_infos
      track_fixture(%{
        artist_name: "Thin Lizzy",
        artist_musicbrainz_id: artist_mbid,
        title: "The Boys Are Back in Town",
        scrobbled_at_uts: now_unix - 100
      })

      track_fixture(%{
        artist_name: "Thin Lizzy",
        artist_musicbrainz_id: artist_mbid,
        title: "Jailbreak",
        scrobbled_at_uts: now_unix - 200
      })

      # One track with empty musicbrainz_id
      track_fixture(%{
        artist_name: "Thin Lizzy",
        artist_musicbrainz_id: "",
        title: "Rosalie / Cowgirl's Song",
        scrobbled_at_uts: now_unix - 300
      })

      results =
        ListeningStats.get_top_artists_by_days(7,
          limit: 10,
          current_time: now,
          timezone: "Etc/UTC"
        )

      assert [thin_lizzy] = Enum.filter(results, fn r -> r.name == "Thin Lizzy" end)
      assert thin_lizzy.play_count == 3
    end
  end

  defp list_tracks do
    ListeningStats.list_tracks()
    |> Enum.map(fn r -> r.track end)
  end

  defp list_tracks(params) do
    ListeningStats.list_tracks(params)
    |> Enum.map(fn r -> r.track end)
  end
end
