/* TestDatabase.cpp */
/*
 * Copyright (C) 2011-2024 Michael Lugmair
 *
 * This file is part of sayonara player
 *
 * This program 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.

 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "Common/LibraryDatabaseProvider.h"
#include "test/Common/SayonaraTest.h"
#include "test/Common/TestTracks.h"

#include "Database/Connector.h"
#include "Database/LibraryDatabase.h"

#include "Utils/Algorithm.h"
#include "Utils/MetaData/MetaDataList.h"
#include "Utils/MetaData/Artist.h"
#include "Utils/MetaData/Album.h"
#include "Utils/MetaData/Genre.h"
#include "Utils/Library/Filter.h"
#include "Utils/Set.h"
#include "Utils/Settings/Settings.h"

namespace
{
	constexpr const auto SearchMode = (+Library::SearchMode::CaseInsensitve | +Library::SearchMode::NoSpecialChars);
	constexpr const auto TestLibraryId = 0;

	Library::Filter createFulltextFilter(const QString& str)
	{
		Library::Filter filter;
		filter.setFiltertext(str);
		filter.setMode(Library::Filter::Mode::Fulltext);

		return filter;
	}

	Album getFirstAlbumByName(const DB::LibraryDatabase* db, const QString& name)
	{
		const auto albums = db->getAllAlbums(true);
		const auto index = Util::Algorithm::indexOf(albums, [&](const auto& album) {
			return (album.name() == name);
		});

		return albums[index];
	}
}

class TracksTest :
	public Test::Base
{
		Q_OBJECT

	public:
		TracksTest() :
			Test::Base("TracksTest")
		{
			Settings::instance()->set<Set::Lib_SearchMode>(SearchMode);
		}

	private:
		Test::LibraryDatabaseProvider initDatabase(const QList<Test::MetaDataBlock>& data)
		{
			return Test::LibraryDatabaseProvider(TestLibraryId, tempPath(), data);
		}

	private slots:
		[[maybe_unused]] void testGetAllTracks()
		{
			const auto dbProvider = initDatabase({
				{"album", "artist", "title"},
				{"album", "artist", "title2"},
				{"album", "artist", "title3"},
			});

			const auto* db = dbProvider.libraryDatabase();

			const auto tracks = db->getAllTracks();
			QVERIFY(tracks.size() == 3);
		}

		[[maybe_unused]] void testFetchByAlbumArtist()
		{
			constexpr const auto artistName = "artist";
			const auto dbProvider = initDatabase({
				{"album", artistName, "title"},
				{"album", artistName, "title2"},
				{"album", artistName, "title3"},
				{"otherAlbum", "otherArtist", "title3"}
			});

			const auto* db = dbProvider.libraryDatabase();
			const auto artistId = db->getArtistID(artistName);
			const auto tracks = db->getAllTracksByArtist({artistId});

			QVERIFY(tracks.size() == 3);
		}

		[[maybe_unused]] void testFetchByArtist()
		{
			const auto dbProvider = initDatabase({
				{"album", "artist", "title"},
				{"album", "artist", "title2"},
				{"album", "artist", "title3"},
				{"album2", "none", "title3"},
			});

			const auto* db = dbProvider.libraryDatabase();
			const auto artistId = db->getArtistID("artist");
			const auto tracks = db->getAllTracksByArtist({artistId});

			QVERIFY(tracks.size() == 3);
		}

		[[maybe_unused]] void testFetchByArtistWithFilter()
		{
			struct TestCase
			{
				QString filterText;
				int expectedTracks;
			};

			const auto testCases = std::array{
				TestCase{"artist", 3},
				TestCase{"none", 0},
				TestCase{"unknown", 0},
				TestCase{"tit", 3},
				TestCase{"title", 2},
			};

			for(const auto& testCase: testCases)
			{
				const auto dbProvider = initDatabase({
					{"album", "artist", "tit1"},
					{"album", "artist", "title2"},
					{"album", "artist", "title3"},
					{"album2", "none", "title3"},
				});
				const auto* db = dbProvider.libraryDatabase();

				const auto artistId = db->getArtistID("artist");
				const auto filter = createFulltextFilter(testCase.filterText);

				const auto tracks = db->getAllTracksByArtist({artistId}, filter);

				QVERIFY(tracks.count() == testCase.expectedTracks);
			}
		}

		[[maybe_unused]] void testFetchByAlbum()
		{
			const auto dbProvider = initDatabase({
				{"album", "artist", "title"},
				{"album", "artist", "title2"},
				{"album", "artist", "title3"},
				{"album2", "none", "title3"},
			});
			const auto* db = dbProvider.libraryDatabase();
			const auto album = getFirstAlbumByName(db, "album");
			const auto albumId = album.id();
			const auto tracks = db->getAllTracksByAlbum({albumId});

			QVERIFY(tracks.count() == 3);
		}

		[[maybe_unused]] void testFetchByAlbumWithFilter()
		{
			constexpr const auto albumName = "album";

			struct TestCase
			{
				QString filterText;
				int expectedTracks;
			};

			const auto testCases = std::array{
				TestCase{albumName, 3},
				TestCase{"bla", 0},
				TestCase{"unknown", 0},
				TestCase{"tit", 3},
				TestCase{"title", 2},
			};

			for(const auto& testCase: testCases)
			{
				const auto dbProvider = initDatabase({
					{albumName, "artist", "tit1"},
					{albumName, "artist", "title2"},
					{albumName, "artist", "title3"},
					{"bla", "artist2", "title4"},
				});

				const auto* db = dbProvider.libraryDatabase();
				const auto album = getFirstAlbumByName(db, albumName);
				const auto albumId = album.id();

				const auto filter = createFulltextFilter(testCase.filterText);
				const auto tracks = db->getAllTracksByAlbum({albumId}, filter, -1);

				QVERIFY(tracks.count() == testCase.expectedTracks);
			}
		}

		[[maybe_unused]] void testSearchByFulltext()
		{
			struct TestCase
			{
				QString filterText;
				int expectedTracks;
			};

			const auto testCases = std::array{
				TestCase{"title", 3},
				TestCase{"tit", 4},
				TestCase{"title2", 1},
				TestCase{"none", 0},
				TestCase{"album", 3},
				TestCase{"artist", 4},
				TestCase{"artist2", 1},
			};

			for(const auto& testCase: testCases)
			{
				const auto dbProvider = initDatabase({
					{"album", "artist", "tit1"},
					{"album", "artist", "title2"},
					{"album", "artist", "title3"},
					{"bla", "artist2", "title4"},
				});
				const auto* db = dbProvider.libraryDatabase();
				const auto filter = createFulltextFilter(testCase.filterText);
				const auto tracks = db->getAllTracksBySearchString(filter);
				QVERIFY(tracks.count() == testCase.expectedTracks);
			}
		}

		[[maybe_unused]] void testSearchByGenre()
		{
			Library::Filter filter;
			filter.setFiltertext("genre");
			filter.setMode(Library::Filter::Mode::Genre);

			const auto genres = QStringList{"genre1", "genre2", "genre3", "nix"};
			const auto dbProvider = initDatabase({
								{"album", "artist", "tit1"},
								{"album", "artist", "title2"},
								{"album", "artist", "title3"},
								{"bla", "artist2", "title4"},
							});

			auto* db = dbProvider.libraryDatabase();

			auto tracks = db->getAllTracks();
			for(int i = 0; i < tracks.count(); i++)
			{
				QVERIFY(tracks.count() == 4);

				tracks[i].addGenre(Genre{genres[i]});
				db->updateTrack(tracks[i]);
			}

			const auto tracksByGenre = db->getAllTracksBySearchString(filter);
			QVERIFY(tracksByGenre.count() == 3);
		}

		[[maybe_unused]] void testSearchByFilepath()
		{
			struct TestCase
			{
				QString filterText;
				int expectedTracks;
			};

			const auto testCases = std::array{
				TestCase{"album", 3},
				TestCase{"bla", 1},
				TestCase{"artist", 4},
				TestCase{"tit", 4},
				TestCase{"nix", 0}
			};

			for(const auto& testCase: testCases)
			{
				Library::Filter filter;
				filter.setFiltertext(testCase.filterText);
				filter.setMode(Library::Filter::Mode::Filename);

				const auto dbProvider = initDatabase({
					{"album", "artist", "tit1"},
					{"album", "artist", "title2"},
					{"album", "artist", "title3"},
					{"bla", "artist2", "title4"},
				});

				const auto* db = dbProvider.libraryDatabase();

				const auto tracksByFilepath = db->getAllTracksBySearchString(filter);
				QVERIFY(tracksByFilepath.count() == testCase.expectedTracks);
			}
		}

		[[maybe_unused]] void testGetAllGenres()
		{
			const auto dbProvider = initDatabase({
				{"album", "artist", "tit1"},
				{"album", "artist", "title2"},
				{"album", "artist", "title3"},
				{"bla", "artist2", "title4"},
			});

			auto genres = QStringList{"genre1", "genre2", "genre3", "nix"};
			genres.sort();

			auto* db = dbProvider.libraryDatabase();
			auto tracks = db->getAllTracks();
			QVERIFY(tracks.size() == 4);

			for(int i = 0; i < tracks.count(); i++)
			{
				tracks[i].addGenre(Genre{genres[i]});
				const auto success = db->updateTrack(tracks[i]);
				QVERIFY(success);
			}

			auto names = QStringList{};
			{
				const auto fetchedGenres = db->getAllGenres();
				Util::Algorithm::transform(fetchedGenres, names, [](const auto& genre) { return genre.name(); });
				names.removeAll({}); // there's always an empty genre
				names.sort();
			}

			QVERIFY(genres == names);
		}

		[[maybe_unused]] void testGetByPaths()
		{
			const auto dbProvider = initDatabase({
				{"album", "artist", "tit1"},
				{"album", "artist", "title2"},
				{"album", "artist", "title3"},
				{"bla", "artist2", "title4"},
			});

			const auto* db = dbProvider.libraryDatabase();
			const auto tracks = db->getAllTracks();
			auto paths = QStringList{};
			Util::Algorithm::transform(tracks, paths, [](const auto& track) { return track.filepath(); });

			struct TestCase
			{
				QStringList paths;
				int expectedTracks;
			};

			const auto testCases = std::array{
				TestCase{paths, 4},
				TestCase{paths.mid(0, 1), 1},
				TestCase{paths.mid(0, 1) + paths.mid(0, 1), 1},
				TestCase{paths.mid(0, 2), 2},
				TestCase{paths.mid(0, 3), 3},
				TestCase{paths.mid(0, 3) << "i don't know", 3},
			};

			for(const auto& testCase: testCases)
			{
				const auto fetchedTracks = db->getAllTracksByPaths(testCase.paths);
				QVERIFY(testCase.expectedTracks == fetchedTracks.count());
			}
		}

		[[maybe_unused]] void testInsertAndUpdate() // NOLINT(readability-function-cognitive-complexity)
		{
			const auto originalTrack = Test::createTrack(1, "title", "artist", "album", "albumArtist");
			const auto path = originalTrack.filepath();

			const auto dbProvider = initDatabase({});
			auto* db = dbProvider.libraryDatabase();
			const auto success = db->storeMetadata(MetaDataList{originalTrack});

			QVERIFY(success);

			{
				auto track = db->getTrackByPath(path);
				QVERIFY(track.id() >= 0);
				QVERIFY(track.title() == "title");
				QVERIFY(track.artist() == "artist");
				QVERIFY(track.album() == "album");
				QVERIFY(track.albumArtist() == "albumArtist");

				track.setBitrate(128'000); // NOLINT(readability-magic-numbers)
				track.setComment("newComment");
				track.setDiscnumber(5); // NOLINT(readability-magic-numbers)
				track.setFilesize(200'000); // NOLINT(readability-magic-numbers)
				track.setGenres(QStringList() << "newGenre1" << "newGenre2");
				track.setDurationMs(200'000); // NOLINT(readability-magic-numbers)
				track.setRating(Rating::One);
				track.setTitle("newTitle");
				track.setTrackNumber(2);
				track.setYear(1994); // NOLINT(readability-magic-numbers)

				QVERIFY(db->updateTrack(track));
			}

			{
				const auto track = db->getTrackByPath(path);
				QVERIFY(track.id() >= 0);
				QVERIFY(track.bitrate() == 128'000);
				QVERIFY(track.comment() == "newComment");
				QVERIFY(track.discnumber() == 5);
				QVERIFY(track.filesize() == 200'000);
				QVERIFY(track.genres().size() == 2);
				QVERIFY(track.genres().contains(Genre("newGenre1")));
				QVERIFY(track.genres().contains(Genre("newGenre2")));
				QVERIFY(track.durationMs() == 200'000);
				QVERIFY(track.rating() == Rating::One);
				QVERIFY(track.title() == "newTitle");
				QVERIFY(track.trackNumber() == 2);
				QVERIFY(track.year() == 1994);
			}
		}

		[[maybe_unused]] void testInsertAndDelete()
		{
			const auto originalTrack =
				Test::createTrack(1, "titleToDelete", "artistToDelete", "albumToDelete", "albumArtistToDelete");
			const auto path = originalTrack.filepath();

			const auto dbProvider = initDatabase({});
			auto* db = dbProvider.libraryDatabase();
			const auto success = db->storeMetadata(MetaDataList{originalTrack});
			QVERIFY(success);

			{
				const auto track = db->getTrackByPath(path);
				QVERIFY(track.id() >= 0);
				QVERIFY(!track.isEqualDeep(MetaData {}));
				db->deleteTrack(track.id());
			}

			{
				const auto deletedTrack = db->getTrackByPath(path);
				QVERIFY(deletedTrack.id() < 0);
				QVERIFY(deletedTrack.isEqualDeep(MetaData {}));
			}
		}

		[[maybe_unused]] void testRenameFilepath()
		{
			const auto originalTrack =
				Test::createTrack(1, "titleToRename", "artistToRename", "albumToRename", "albumArtistToRename");
			const auto path = originalTrack.filepath();
			const auto newPath = QStringLiteral("/some/new/path.mp3");

			const auto dbProvider = initDatabase({});
			auto* db = dbProvider.libraryDatabase();
			const auto success = db->storeMetadata(MetaDataList{originalTrack});

			QVERIFY(success);

			{
				const auto track = db->getTrackByPath(path);
				QVERIFY(track.id() >= 0);
				db->renameFilepath(path, newPath, track.libraryId());
			}

			{
				const auto nonExistentTrack = db->getTrackByPath(path);
				QVERIFY(nonExistentTrack.isEqualDeep(MetaData {}));

				const auto trackWithNewPath = db->getTrackByPath(newPath);
				QVERIFY(trackWithNewPath.id() >= 0);
				QVERIFY(trackWithNewPath.filepath() == newPath);
			}
		}
};

QTEST_GUILESS_MAIN(TracksTest)

#include "TracksTest.moc"
