/*
 * Strawberry Music Player
 * This file was part of Clementine.
 * Copyright 2010, David Sansome <me@davidsansome.com>
 * Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
 *
 * Strawberry is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Strawberry is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Strawberry.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <memory>

#include <gtest/gtest.h>

#include <QFileInfo>
#include <QSignalSpy>
#include <QThread>
#include <QtDebug>

#include "core/scoped_ptr.h"
#include "core/shared_ptr.h"
#include "core/song.h"
#include "core/database.h"
#include "utilities/timeconstants.h"
#include "collection/collectionbackend.h"
#include "collection/collection.h"

using std::make_unique;

// clazy:excludeall=non-pod-global-static,returning-void-expression

namespace {

class CollectionBackendTest : public ::testing::Test {
 protected:
  void SetUp() override {
    database_.reset(new MemoryDatabase(nullptr));
    backend_ = make_unique<CollectionBackend>();
    backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kFtsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable));
  }

  static Song MakeDummySong(int directory_id) {
    // Returns a valid song with all the required fields set
    Song ret;
    ret.set_directory_id(directory_id);
    ret.set_url(QUrl::fromLocalFile(QStringLiteral("foo.flac")));
    ret.set_mtime(1);
    ret.set_ctime(1);
    ret.set_filesize(1);
    return ret;
  }

  SharedPtr<Database> database_;  // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
  ScopedPtr<CollectionBackend> backend_;  // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
};

TEST_F(CollectionBackendTest, EmptyDatabase) {

  // Check the database is empty to start with
  QStringList artists = backend_->GetAllArtists();
  EXPECT_TRUE(artists.isEmpty());

  CollectionBackend::AlbumList albums = backend_->GetAllAlbums();
  EXPECT_TRUE(albums.isEmpty());

}

TEST_F(CollectionBackendTest, AddDirectory) {

  QSignalSpy spy(&*backend_, &CollectionBackend::DirectoryDiscovered);

  backend_->AddDirectory(QStringLiteral("/tmp"));

  // Check the signal was emitted correctly
  ASSERT_EQ(1, spy.count());
  CollectionDirectory dir = spy[0][0].value<CollectionDirectory>();
  EXPECT_EQ(QFileInfo(QStringLiteral("/tmp")).canonicalFilePath(), dir.path);
  EXPECT_EQ(1, dir.id);
  EXPECT_EQ(0, spy[0][1].value<CollectionSubdirectoryList>().size());

}

TEST_F(CollectionBackendTest, RemoveDirectory) {

  // Add a directory
  CollectionDirectory dir;
  dir.id = 1;
  dir.path = QStringLiteral("/tmp");
  backend_->AddDirectory(dir.path);

  QSignalSpy spy(&*backend_, &CollectionBackend::DirectoryDeleted);

  // Remove the directory again
  backend_->RemoveDirectory(dir);

  // Check the signal was emitted correctly
  ASSERT_EQ(1, spy.count());
  dir = spy[0][0].value<CollectionDirectory>();
  EXPECT_EQ(QStringLiteral("/tmp"), dir.path);
  EXPECT_EQ(1, dir.id);

}

TEST_F(CollectionBackendTest, GetAlbumArtNonExistent) {}

// Test adding a single song to the database, then getting various information back about it.
class SingleSong : public CollectionBackendTest {
 protected:
  void SetUp() override {
    CollectionBackendTest::SetUp();

    // Add a directory - this will get ID 1
    backend_->AddDirectory(QStringLiteral("/tmp"));

    // Make a song in that directory
    song_ = MakeDummySong(1);
    song_.set_title(QStringLiteral("Title"));
    song_.set_artist(QStringLiteral("Artist"));
    song_.set_album(QStringLiteral("Album"));
    song_.set_url(QUrl::fromLocalFile(QStringLiteral("foo.flac")));
  }

  void AddDummySong() {
    QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsDiscovered);
    QSignalSpy deleted_spy(&*backend_, &CollectionBackend::SongsDeleted);

    // Add the song
    backend_->AddOrUpdateSongs(SongList() << song_);

    // Check the correct signals were emitted
    EXPECT_EQ(0, deleted_spy.count());
    ASSERT_EQ(1, added_spy.count());

    SongList list = *(reinterpret_cast<SongList*>(added_spy[0][0].data()));
    ASSERT_EQ(1, list.count());
    EXPECT_EQ(song_.title(), list[0].title());
    EXPECT_EQ(song_.artist(), list[0].artist());
    EXPECT_EQ(song_.album(), list[0].album());
    EXPECT_EQ(1, list[0].id());
    EXPECT_EQ(1, list[0].directory_id());
  }

  Song song_;  // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)

};

TEST_F(SingleSong, GetSongWithNoAlbum) {

  song_.set_album(QLatin1String(""));
  AddDummySong();
  if (HasFatalFailure()) return;

  EXPECT_EQ(1, backend_->GetAllArtists().size());
  CollectionBackend::AlbumList albums = backend_->GetAllAlbums();
  EXPECT_EQ(1, albums.size());
  EXPECT_EQ(QStringLiteral("Artist"), albums[0].album_artist);
  EXPECT_EQ(QLatin1String(""), albums[0].album);

}

TEST_F(SingleSong, GetAllArtists) {

  AddDummySong();
  if (HasFatalFailure()) return;

  QStringList artists = backend_->GetAllArtists();
  ASSERT_EQ(1, artists.size());
  EXPECT_EQ(song_.artist(), artists[0]);

}

TEST_F(SingleSong, GetAllAlbums) {

  AddDummySong();
  if (HasFatalFailure()) return;

  CollectionBackend::AlbumList albums = backend_->GetAllAlbums();
  ASSERT_EQ(1, albums.size());
  EXPECT_EQ(song_.album(), albums[0].album);
  EXPECT_EQ(song_.artist(), albums[0].album_artist);

}

TEST_F(SingleSong, GetAlbumsByArtist) {

  AddDummySong();
  if (HasFatalFailure()) return;

  CollectionBackend::AlbumList albums = backend_->GetAlbumsByArtist(QStringLiteral("Artist"));
  ASSERT_EQ(1, albums.size());
  EXPECT_EQ(song_.album(), albums[0].album);
  EXPECT_EQ(song_.artist(), albums[0].album_artist);

}

TEST_F(SingleSong, GetAlbumArt) {

  AddDummySong();
  if (HasFatalFailure()) return;

  CollectionBackend::Album album = backend_->GetAlbumArt(QStringLiteral("Artist"), QStringLiteral("Album"));
  EXPECT_EQ(song_.album(), album.album);
  EXPECT_EQ(song_.effective_albumartist(), album.album_artist);

}

TEST_F(SingleSong, GetSongs) {

  AddDummySong();
  if (HasFatalFailure()) return;

  SongList songs = backend_->GetAlbumSongs(QStringLiteral("Artist"), QStringLiteral("Album"));
  ASSERT_EQ(1, songs.size());
  EXPECT_EQ(song_.album(), songs[0].album());
  EXPECT_EQ(song_.artist(), songs[0].artist());
  EXPECT_EQ(song_.title(), songs[0].title());
  EXPECT_EQ(1, songs[0].id());

}

TEST_F(SingleSong, GetSongById) {

  AddDummySong();
  if (HasFatalFailure()) return;

  Song song = backend_->GetSongById(1);
  EXPECT_EQ(song_.album(), song.album());
  EXPECT_EQ(song_.artist(), song.artist());
  EXPECT_EQ(song_.title(), song.title());
  EXPECT_EQ(1, song.id());

}

TEST_F(SingleSong, FindSongsInDirectory) {

  AddDummySong();
  if (HasFatalFailure()) return;

  SongList songs = backend_->FindSongsInDirectory(1);
  ASSERT_EQ(1, songs.size());
  EXPECT_EQ(song_.album(), songs[0].album());
  EXPECT_EQ(song_.artist(), songs[0].artist());
  EXPECT_EQ(song_.title(), songs[0].title());
  EXPECT_EQ(1, songs[0].id());

}

TEST_F(SingleSong, UpdateSong) {

  AddDummySong();
  if (HasFatalFailure()) return;

  Song new_song(song_);
  new_song.set_id(1);
  new_song.set_title(QStringLiteral("A different title"));

  QSignalSpy deleted_spy(&*backend_, &CollectionBackend::SongsDeleted);
  QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsDiscovered);

  backend_->AddOrUpdateSongs(SongList() << new_song);

  ASSERT_EQ(1, added_spy.size());
  ASSERT_EQ(1, deleted_spy.size());

  SongList songs_added = *(reinterpret_cast<SongList*>(added_spy[0][0].data()));
  SongList songs_deleted = *(reinterpret_cast<SongList*>(deleted_spy[0][0].data()));
  ASSERT_EQ(1, songs_added.size());
  ASSERT_EQ(1, songs_deleted.size());
  EXPECT_EQ(QStringLiteral("Title"), songs_deleted[0].title());
  EXPECT_EQ(QStringLiteral("A different title"), songs_added[0].title());
  EXPECT_EQ(1, songs_deleted[0].id());
  EXPECT_EQ(1, songs_added[0].id());

}

TEST_F(SingleSong, DeleteSongs) {

  AddDummySong();
  if (HasFatalFailure()) return;

  Song new_song(song_);
  new_song.set_id(1);

  QSignalSpy deleted_spy(&*backend_, &CollectionBackend::SongsDeleted);

  backend_->DeleteSongs(SongList() << new_song);

  ASSERT_EQ(1, deleted_spy.size());

  SongList songs_deleted = *(reinterpret_cast<SongList*>(deleted_spy[0][0].data()));
  ASSERT_EQ(1, songs_deleted.size());
  EXPECT_EQ(QStringLiteral("Title"), songs_deleted[0].title());
  EXPECT_EQ(1, songs_deleted[0].id());

  // Check we can't retrieve that song any more
  Song song = backend_->GetSongById(1);
  EXPECT_FALSE(song.is_valid());
  EXPECT_EQ(-1, song.id());

  // And the artist or album shouldn't show up either
  QStringList artists = backend_->GetAllArtists();
  EXPECT_EQ(0, artists.size());

  CollectionBackend::AlbumList albums = backend_->GetAllAlbums();
  EXPECT_EQ(0, albums.size());

}

TEST_F(SingleSong, MarkSongsUnavailable) {

  AddDummySong();
  if (HasFatalFailure()) return;

  Song new_song(song_);
  new_song.set_id(1);

  QSignalSpy deleted_spy(&*backend_, &CollectionBackend::SongsDeleted);

  backend_->MarkSongsUnavailable(SongList() << new_song);

  ASSERT_EQ(1, deleted_spy.size());

  SongList songs_deleted = *(reinterpret_cast<SongList*>(deleted_spy[0][0].data()));
  ASSERT_EQ(1, songs_deleted.size());
  EXPECT_EQ(QStringLiteral("Title"), songs_deleted[0].title());
  EXPECT_EQ(1, songs_deleted[0].id());

  // Check the song is marked as deleted.
  Song song = backend_->GetSongById(1);
  EXPECT_TRUE(song.is_valid());
  EXPECT_TRUE(song.unavailable());

  // And the artist or album shouldn't show up either
  QStringList artists = backend_->GetAllArtists();
  EXPECT_EQ(0, artists.size());

  CollectionBackend::AlbumList albums = backend_->GetAllAlbums();
  EXPECT_EQ(0, albums.size());

}

class TestUrls : public CollectionBackendTest {
 protected:
  void SetUp() override {
    CollectionBackendTest::SetUp();
    backend_->AddDirectory(QStringLiteral("/mnt/music"));
  }
};

TEST_F(TestUrls, TestUrls) {

  QStringList strings = QStringList() << QStringLiteral("file:///mnt/music/01 - Pink Floyd - Echoes.flac")
                                      << QStringLiteral("file:///mnt/music/02 - Björn Afzelius - Det räcker nu.flac")
                                      << QStringLiteral("file:///mnt/music/03 - Vazelina Bilopphøggers - Bomull i øra.flac")
                                      << QStringLiteral("file:///mnt/music/Test !#$%&'()-@^_`{}~..flac");

  const QList<QUrl> urls = QUrl::fromStringList(strings);
  SongList songs;
  songs.reserve(urls.count());
  for (const QUrl &url : urls) {

    EXPECT_EQ(url, QUrl::fromEncoded(url.toString(QUrl::FullyEncoded).toUtf8()));
    EXPECT_EQ(url.toString(QUrl::FullyEncoded), QString::fromLatin1(url.toEncoded()));

    Song song(Song::Source::Collection);
    song.set_directory_id(1);
    song.set_title(QStringLiteral("Test Title"));
    song.set_album(QStringLiteral("Test Album"));
    song.set_artist(QStringLiteral("Test Artist"));
    song.set_url(url);
    song.set_length_nanosec(kNsecPerSec);
    song.set_mtime(1);
    song.set_ctime(1);
    song.set_filesize(1);
    song.set_valid(true);

    songs << song;

  }

  QSignalSpy spy(&*backend_, &CollectionBackend::SongsDiscovered);

  backend_->AddOrUpdateSongs(songs);
  if (HasFatalFailure()) return;

  ASSERT_EQ(1, spy.count());
  SongList new_songs = spy[0][0].value<SongList>();
  EXPECT_EQ(new_songs.count(), strings.count());

  for (const QUrl &url : urls) {

    songs = backend_->GetSongsByUrl(url);
    EXPECT_EQ(1, songs.count());
    if (songs.count() < 1) continue;

    Song new_song = songs.first();
    EXPECT_TRUE(new_song.is_valid());
    EXPECT_EQ(new_song.url(), url);

    new_song = backend_->GetSongByUrl(url);
    EXPECT_EQ(1, songs.count());
    if (songs.count() < 1) continue;

    EXPECT_TRUE(new_song.is_valid());
    EXPECT_EQ(new_song.url(), url);

    QSqlDatabase db(database_->Connect());
    QSqlQuery q(db);
    q.prepare(QStringLiteral("SELECT url FROM %1 WHERE url = :url").arg(QLatin1String(SCollection::kSongsTable)));

    q.bindValue(QStringLiteral(":url"), url.toString(QUrl::FullyEncoded));
    EXPECT_TRUE(q.exec());

    while (q.next()) {
      EXPECT_EQ(url, q.value(0).toUrl());
      EXPECT_EQ(url, QUrl::fromEncoded(q.value(0).toByteArray()));
    }

  }

}

class UpdateSongsBySongID : public CollectionBackendTest {
 protected:
  void SetUp() override {
    CollectionBackendTest::SetUp();
    backend_->AddDirectory(QStringLiteral("/mnt/music"));
  }
};

TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) {

    const QStringList song_ids = QStringList() << QStringLiteral("song1")
                                               << QStringLiteral("song2")
                                               << QStringLiteral("song3")
                                               << QStringLiteral("song4")
                                               << QStringLiteral("song5")
                                               << QStringLiteral("song6");

  { // Add songs
    SongMap songs;

    for (const QString &song_id : song_ids) {

      QUrl url;
      url.setScheme(QStringLiteral("file"));
      url.setPath(QStringLiteral("/music/") + song_id);

      Song song(Song::Source::Collection);
      song.set_song_id(song_id);
      song.set_directory_id(1);
      song.set_title(QStringLiteral("Test Title ") + song_id);
      song.set_album(QStringLiteral("Test Album"));
      song.set_artist(QStringLiteral("Test Artist"));
      song.set_url(url);
      song.set_length_nanosec(kNsecPerSec);
      song.set_mtime(1);
      song.set_ctime(1);
      song.set_filesize(1);
      song.set_valid(true);

      songs.insert(song_id, song);

    }

    QSignalSpy spy(&*backend_, &CollectionBackend::SongsDiscovered);

    backend_->UpdateSongsBySongID(songs);

    ASSERT_EQ(1, spy.count());
    SongList new_songs = spy[0][0].value<SongList>();
    EXPECT_EQ(new_songs.count(), song_ids.count());
    EXPECT_EQ(song_ids[0], new_songs[0].song_id());
    EXPECT_EQ(song_ids[1], new_songs[1].song_id());
    EXPECT_EQ(song_ids[2], new_songs[2].song_id());
    EXPECT_EQ(song_ids[3], new_songs[3].song_id());
    EXPECT_EQ(song_ids[4], new_songs[4].song_id());
    EXPECT_EQ(song_ids[5], new_songs[5].song_id());

  }

  {  // Check that all songs are added.

    SongMap songs;
    {
      QSqlDatabase db(database_->Connect());
      CollectionQuery query(db, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kFtsTable));
      EXPECT_TRUE(backend_->ExecCollectionQuery(&query, songs));
    }

    EXPECT_EQ(songs.count(), song_ids.count());

    for (SongMap::const_iterator it = songs.constBegin() ; it != songs.constEnd() ; ++it) {
      EXPECT_EQ(it.key(), it.value().song_id());
    }

    for (const QString &song_id : song_ids) {
      EXPECT_TRUE(songs.contains(song_id));
    }

  }

  {  // Remove some songs
    QSignalSpy spy1(&*backend_, &CollectionBackend::SongsDiscovered);
    QSignalSpy spy2(&*backend_, &CollectionBackend::SongsDeleted);

    SongMap songs;

    const QStringList song_ids2 = QStringList() << QStringLiteral("song1")
                                                << QStringLiteral("song4")
                                                << QStringLiteral("song5")
                                                << QStringLiteral("song6");

    for (const QString &song_id : song_ids2) {

      QUrl url;
      url.setScheme(QStringLiteral("file"));
      url.setPath(QStringLiteral("/music/") + song_id);

      Song song(Song::Source::Collection);
      song.set_song_id(song_id);
      song.set_directory_id(1);
      song.set_title(QStringLiteral("Test Title ") + song_id);
      song.set_album(QStringLiteral("Test Album"));
      song.set_artist(QStringLiteral("Test Artist"));
      song.set_url(url);
      song.set_length_nanosec(kNsecPerSec);
      song.set_mtime(1);
      song.set_ctime(1);
      song.set_filesize(1);
      song.set_valid(true);

      songs.insert(song_id, song);

    }

    backend_->UpdateSongsBySongID(songs);

    ASSERT_EQ(0, spy1.count());
    ASSERT_EQ(1, spy2.count());
    SongList deleted_songs = spy2[0][0].value<SongList>();
    EXPECT_EQ(deleted_songs.count(), 2);
    EXPECT_EQ(deleted_songs[0].song_id(), QStringLiteral("song2"));
    EXPECT_EQ(deleted_songs[1].song_id(), QStringLiteral("song3"));

  }

  {  // Update some songs
    QSignalSpy spy1(&*backend_, &CollectionBackend::SongsDeleted);
    QSignalSpy spy2(&*backend_, &CollectionBackend::SongsDiscovered);

    SongMap songs;

    const QStringList song_ids2 = QStringList() << QStringLiteral("song1")
                                                << QStringLiteral("song4")
                                                << QStringLiteral("song5")
                                                << QStringLiteral("song6");

    for (const QString &song_id : song_ids2) {

      QUrl url;
      url.setScheme(QStringLiteral("file"));
      url.setPath(QStringLiteral("/music/") + song_id);

      Song song(Song::Source::Collection);
      song.set_song_id(song_id);
      song.set_directory_id(1);
      song.set_title(QStringLiteral("Test Title ") + song_id);
      song.set_album(QStringLiteral("Test Album"));
      song.set_artist(QStringLiteral("Test Artist"));
      song.set_url(url);
      song.set_length_nanosec(kNsecPerSec);
      song.set_mtime(1);
      song.set_ctime(1);
      song.set_filesize(1);
      song.set_valid(true);

      songs.insert(song_id, song);

    }

    songs[QStringLiteral("song1")].set_artist(QStringLiteral("New artist"));
    songs[QStringLiteral("song6")].set_artist(QStringLiteral("New artist"));

    backend_->UpdateSongsBySongID(songs);

    ASSERT_EQ(1, spy1.count());
    ASSERT_EQ(1, spy2.count());
    SongList deleted_songs = spy1[0][0].value<SongList>();
    SongList added_songs = spy2[0][0].value<SongList>();
    EXPECT_EQ(deleted_songs.count(), 2);
    EXPECT_EQ(added_songs.count(), 2);
    EXPECT_EQ(deleted_songs[0].song_id(), QStringLiteral("song1"));
    EXPECT_EQ(deleted_songs[1].song_id(), QStringLiteral("song6"));
    EXPECT_EQ(added_songs[0].song_id(), QStringLiteral("song1"));
    EXPECT_EQ(added_songs[1].song_id(), QStringLiteral("song6"));

  }

}

} // namespace
