/* PlaylistHandlerTest.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/SayonaraTest.h"
#include "Common/PlayManagerMock.h"
#include "Common/PlaylistMocks.h"
#include "Common/TestTracks.h"
#include "Common/FileSystemMock.h"

#include "Components/PlayManager/PlayManager.h"
#include "Components/Playlist/LocalPathPlaylistCreator.h"
#include "Components/Playlist/Playlist.h"
#include "Components/Playlist/PlaylistHandler.h"
#include "Components/Playlist/PlaylistModifiers.h"
#include "Utils/FileUtils.h"
#include "Utils/MetaData/MetaData.h"
#include "Utils/MetaData/MetaDataList.h"
#include "Utils/Settings/Settings.h"

#include <QSignalSpy>

#include <memory>
#include <utility>

// access working directory with Test::Base::tempPath("somefile.txt");

namespace
{
	class TestPlayManager :
		public Test::PlayManagerMock
	{
		public:
			void continueFromStop() override
			{
				emit sigContinueFromStop();
			}

			void stop() override
			{
				emit sigPlaystateChanged(PlayState::Stopped);
			}

			void play() override
			{
				emit sigPlaystateChanged(PlayState::Playing);
			}

			void next() override
			{
				emit sigNext();
			}
	};

	class PlaylistFromPathCreatorMock :
		public Playlist::LocalPathPlaylistCreator
	{
		public:
			PlaylistFromPathCreatorMock(Playlist::Creator* creator, MetaDataList tracks) :
				m_creator {creator},
				m_tracks {std::move(tracks)} {}

			int createPlaylists(const QStringList& /*paths*/, const QString& name, bool temporary) override
			{
				const auto index = m_creator->createPlaylist(m_tracks, name, temporary);
				emit sigAllPlaylistsCreated(index);

				return index;
			}

		private:
			Playlist::Creator* m_creator;
			MetaDataList m_tracks;
	};

	Playlist::LocalPathPlaylistCreator*
	makePlaylistFromPathCreator(Playlist::Creator* creator, const MetaDataList& tracks)
	{
		return new PlaylistFromPathCreatorMock(creator, tracks);
	}

	int
	createPlaylist(const std::shared_ptr<Playlist::Handler>& playlistHandler, const QString& name, const int trackCount)
	{
		auto tracks = MetaDataList {};
		auto count = playlistHandler->count();
		for(int i = 0; i < trackCount; i++)
		{
			const auto filename = QString("/path/to/playlist%1/track%2.mp3").arg(count).arg(i);
			tracks << MetaData {filename};
		}

		return playlistHandler->createPlaylist(tracks, name);
	}

	void
	createPlaylists(const std::shared_ptr<Playlist::Handler>& playlistHandler, const int count, const int trackCount)
	{
		QVERIFY(playlistHandler->count() == 1);
		createPlaylist(playlistHandler, playlistHandler->playlist(0)->name(), trackCount);
		for(int i = 0; i < count - 1; i++) // 1st playlist is created on construction
		{
			const int index = createPlaylist(playlistHandler, QString("playlist%1").arg(i), trackCount);
			QVERIFY(index == i + 1);
			QVERIFY(playlistHandler->count() == i + 2);
		}

		QVERIFY2(playlistHandler->count() == count,
		         "playlist[0] was created automatically by Playlist::Handler");
	}
}

class PlaylistHandlerTest :
	public Test::Base
{
	Q_OBJECT

	public:
		PlaylistHandlerTest() :
			Test::Base("PlaylistHandlerTest"),
			m_playManager(new TestPlayManager()) {}

	private:
		PlayManager* m_playManager;

		std::shared_ptr<Playlist::Handler> createHandler()
		{
			return std::make_shared<Playlist::Handler>(
				m_playManager, std::make_shared<Test::PlaylistLoaderMock>(),
				std::make_shared<Test::AllFilesAvailableFileSystem>());
		}

	private slots: // NOLINT(readability-redundant-access-specifiers)
		[[maybe_unused]] void testIfPlaylistIndicesAreCorrectAfterCreation()
		{
			struct TestCase
			{
				bool override;
				int expectedIndex;
				int expectedCount;
			};

			const auto testCases = std::array {
				TestCase {false, 1, 2},
				TestCase {false, 2, 3},
				TestCase {true, 2, 3}
			};

			auto plh = createHandler();
			QVERIFY(plh->count() == 1);

			for(const auto& testCase: testCases)
			{
				const auto index = plh->createEmptyPlaylist(testCase.override);
				QVERIFY(plh->playlist(index)->index() == index);
				QVERIFY(index == testCase.expectedIndex);
				QVERIFY(plh->count() == testCase.expectedCount);
			}

			plh->shutdown();
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void testIfPlaylistIndicesAreInRange()
		{
			struct TestCase
			{
				int playlistsAvailable;
				QList<int> expectedIndices;
			};

			const auto testCases = std::array {
				TestCase {1, {0}},
				TestCase {2, {0, 1}},
				TestCase {3, {0, 1, 2}}
			};

			for(const auto& testCase: testCases)
			{
				auto plh = createHandler();
				createPlaylists(plh, testCase.playlistsAvailable, 1);
				QVERIFY(plh->count() == testCase.playlistsAvailable);
				for(auto i = 0; i < plh->count(); i++)
				{
					QVERIFY(plh->playlist(i)->index() == testCase.expectedIndices[i]);
				}
			}
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void closeTest() // NOLINT(readability-function-cognitive-complexity)
		{
			struct TestCase
			{
				int playlistsToCreate;
				int closePlaylistIndex;
				QList<int> expectedIndices;
			};

			const auto testCases = std::array {
				TestCase {2, -4, {0, 1}},
				TestCase {2, 5, {0, 1}},
				TestCase {2, 0, {0}},
				TestCase {2, 1, {0}},
				TestCase {1, 0, {0}},
			};

			for(const auto& testCase: testCases)
			{
				auto plh = createHandler();
				createPlaylists(plh, testCase.playlistsToCreate, 1);

				QVERIFY(plh->count() == testCase.playlistsToCreate);
				plh->closePlaylist(testCase.closePlaylistIndex);

				QVERIFY(plh->count() == testCase.expectedIndices.count());

				for(auto i = 0; i < plh->count(); i++)
				{
					QVERIFY(plh->playlist(i)->index() == testCase.expectedIndices[i]);
				}
			}
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void testCurrentIndexAfterCreation()
		{
			constexpr const auto PlaylistsToCreate = 3;
			auto plh = createHandler();
			QVERIFY(plh->currentIndex() == 0);
			for(auto i = 0; i < PlaylistsToCreate; i++)
			{
				plh->createEmptyPlaylist(false);

				QVERIFY(plh->currentIndex() == i + 1);
			}
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void testSetingOfCurrentIndices()
		{
			constexpr const auto PlaylistsToCreate = 3;

			struct TestCase
			{
				int index;
				int expectedIndex;
			};

			const auto testCases = std::array {
				TestCase {0, 0},
				TestCase {1, 1},
				TestCase {2, 2},
				TestCase {-1, PlaylistsToCreate - 1},
				TestCase {4, PlaylistsToCreate - 1},
			};

			for(const auto& testCase: testCases)
			{
				auto plh = createHandler();
				createPlaylists(plh, PlaylistsToCreate, 1);
				plh->setCurrentIndex(testCase.index);

				QVERIFY(plh->currentIndex() == testCase.expectedIndex);
			}
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void testCurrentIndexAfterClosing()
		{
			constexpr const auto PlaylistsToCreate = 3;
			auto plh = createHandler();
			createPlaylists(plh, PlaylistsToCreate, 1);
			plh->setCurrentIndex(1);

			struct TestCase
			{
				int currentIndex;
				int closeIndex;
				int expectedNewCurrentIndex;
			};

			const auto testCases = std::array {
				TestCase {0, 0, 0}, // indices {0, 1, 2}
				TestCase {2, 1, 0}, // {0,1}
				TestCase {1, 0, 0}, // {0}
			};

			for(const auto& testCase: testCases)
			{
				plh->setCurrentIndex(testCase.currentIndex);
				plh->closePlaylist(testCase.closeIndex);

				QVERIFY(plh->currentIndex() == testCase.expectedNewCurrentIndex);
			}
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void createPlaylistFromFiles()
		{
			const auto paths = QStringList() << "path2.m3u" << "path1.mp3" << "path3.pls";
			auto plh = createHandler();
			QVERIFY(plh->count() == 1); // empty playlist

			auto spy = QSignalSpy(plh.get(), &Playlist::Handler::sigNewPlaylistAdded);
			plh->createPlaylist(paths, "Test");

			QVERIFY(spy.count() == 3);
			QVERIFY(plh->count() == 4);

			const auto names = QStringList {"path2", "path3", "Test"};
			auto playlistNames = QStringList {
				plh->playlist(1)->name(),
				plh->playlist(2)->name(),
				plh->playlist(3)->name()
			};

			playlistNames.sort(Qt::CaseInsensitive);
			QVERIFY(names == playlistNames);
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void createCommandLinePlaylistSettings()
		{
			struct TestCase
			{
				bool createExtraPlaylist {false};
				bool setPlaylistName {false};
				QString playlistName;
				int callCount {0};
				int expectedPlaylists {0};
			};

			const auto testCases = {
				TestCase {false, false, {}, 3, 3},
				TestCase {true, false, {}, 3, 1},
				TestCase {true, true, "Extra", 3, 1}
			};

			for(const auto& testCase: testCases)
			{
				SetSetting(Set::PL_CreateFilesystemPlaylist, testCase.createExtraPlaylist);
				SetSetting(Set::PL_SpecifyFileystemPlaylistName, testCase.setPlaylistName);
				SetSetting(Set::PL_FilesystemPlaylistName, testCase.playlistName);

				auto plh = createHandler();

				for(int i = 0; i < testCase.callCount; i++)
				{
					plh->createCommandLinePlaylist({}, makePlaylistFromPathCreator(plh.get(), Test::createTracks()));
				}

				QVERIFY(plh->count() == testCase.expectedPlaylists);
			}
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void createCommandLinePlaylist()
		{
			auto plh = createHandler();
			createPlaylists(plh, 3, 0);
			QVERIFY(plh->count() == 3);

			auto spy = QSignalSpy(plh.get(), &Playlist::Handler::sigNewPlaylistAdded);
			const auto paths = QStringList() << "path2.m3u" << "path1.mp3" << "path3.pls";
			plh->createCommandLinePlaylist(paths, nullptr);
			QVERIFY(spy.count() == 3);
			QVERIFY(plh->count() == 6);
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void testEmptyPlaylistDeletion()
		{
			auto plh = createHandler();

			QVERIFY(plh->count() == 1);
			const auto firstPlaylist = plh->playlist(0);
			QVERIFY(firstPlaylist->tracks().isEmpty());

			const auto playlistName = firstPlaylist->name();
			const auto playlistId = firstPlaylist->id();
			const auto tracks = Test::createTracks();

			const auto index = plh->createCommandLinePlaylist({}, makePlaylistFromPathCreator(plh.get(), tracks));
			const auto newPlaylist = plh->playlist(index);

			QVERIFY(plh->count() == 1);
			QVERIFY(index == 0);
			QVERIFY(newPlaylist->name() != playlistName);
			QVERIFY(newPlaylist->id() != playlistId);
			QVERIFY(newPlaylist->count() == tracks.count());
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void testEmptyPlaylistsDeletedOnShutdown()
		{
			constexpr const auto PlaylistCount = 5;
			constexpr const auto TrackCount = 5;

			auto plh = createHandler();
			createPlaylists(plh, PlaylistCount, TrackCount);

			QVERIFY(plh->count() == PlaylistCount);

			Playlist::clear(*plh->playlist(0), Playlist::Reason::Undefined);
			Playlist::clear(*plh->playlist(2), Playlist::Reason::Undefined);
			Playlist::clear(*plh->playlist(4), Playlist::Reason::Undefined);

			auto spy = QSignalSpy(plh.get(), &Playlist::Handler::sigPlaylistClosed);

			plh->shutdown();

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

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void testIfClosedSignalIsSentBeforeNewPlaylistIsAdded()
		{
			auto plh = createHandler();

			auto isPlaylistAdded = false;
			auto isPlaylistClosed = false;

			connect(plh.get(), &Playlist::Handler::sigNewPlaylistAdded, this, [&](const auto& /*i*/) {
				isPlaylistAdded = true;
				QVERIFY(isPlaylistClosed);
			});

			connect(plh.get(), &Playlist::Handler::sigPlaylistClosed, this, [&]() {
				isPlaylistClosed = true;
				QVERIFY(!isPlaylistAdded);
			});

			plh->closePlaylist(0);
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void testActiveIndexStaysNegativeOnCreation()
		{
			auto plh = createHandler();
			QVERIFY(plh->activeIndex() == -1);

			for(int i = 0; i < 4; i++)
			{
				plh->createEmptyPlaylist();
				QVERIFY(plh->activeIndex() == -1);
			}
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void testActivePlaylistChangesOnPlayAndStop()
		{
			constexpr const auto TrackCount = 5;

			auto plh = createHandler();
			const auto index = createPlaylist(plh, "playlist", TrackCount);
			auto playlist = plh->playlist(index);

			playlist->play();
			QVERIFY(plh->activeIndex() == index);

			playlist->stop();
			QVERIFY(plh->activeIndex() == -1);
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void testActiveIndexBecomesNegativeOnClose()
		{
			struct TestCase
			{
				int playlistCount;
				int activePlaylist;
				int closePlaylistIndex;
				int expectedActivePlaylist;
			};

			const auto testCases = std::array {
				TestCase {2, 0, 0, -1},
				TestCase {2, 0, 1, 0},
				TestCase {2, 1, 0, 0},
				TestCase {2, 1, 1, -1}
			};

			for(const auto& testCase: testCases)
			{
				constexpr const auto TrackCount = 5;

				auto plh = createHandler();
				createPlaylists(plh, testCase.playlistCount, TrackCount);
				QVERIFY(plh->count() == testCase.playlistCount);

				auto playlist = plh->playlist(testCase.activePlaylist);
				QVERIFY(plh->activeIndex() == -1);

				playlist->play();
				QVERIFY(plh->activeIndex() == testCase.activePlaylist);

				plh->closePlaylist(testCase.closePlaylistIndex);
				QVERIFY(plh->activeIndex() == testCase.expectedActivePlaylist);
			}
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void testContinueFromStop()
		{
			constexpr const auto TrackCount = 5;
			struct TestCase
			{
				int playlistCount;
				int tracksInFirstPlaylist;
				int currentIndex;
				int expectedActivePlaylistIndex;
			};

			const auto testCases = std::array {
				TestCase {3, 10, 0, 0},
				TestCase {3, 10, 1, 1},
				TestCase {3, 10, 2, 2},
				TestCase {3, 0, 0, 1},

				TestCase {0, 0, 0, -1},
				TestCase {0, 10, 0, 0}
			};

			for(const auto& testCase: testCases)
			{
				auto plh = createHandler();
				for(int i = 0; i < testCase.playlistCount; i++)
				{
					createPlaylist(plh, QString("playlist%1").arg(i), TrackCount);
				}

				QCOMPARE(plh->count(), testCase.playlistCount + 1); // one implicit playlist
				createPlaylist(plh, plh->playlist(0)->name(), testCase.tracksInFirstPlaylist);

				plh->playlist(0)->play();
				plh->setCurrentIndex(testCase.currentIndex);

				m_playManager->stop();
				m_playManager->continueFromStop();

				QCOMPARE(plh->activeIndex(), testCase.expectedActivePlaylistIndex);
			}
		}

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		[[maybe_unused]] void testPlaylistStopsAfterLastTrackBeforeStop()
		{
			constexpr const auto PlaylistCount = 1;
			constexpr const auto TrackCount = 5;
			constexpr const auto CurrentPlaylistIndex = 0;
			constexpr const auto LastTrackBeforeStop = 3;

			auto plh = createHandler();
			createPlaylists(plh, PlaylistCount, TrackCount);

			plh->setCurrentIndex(CurrentPlaylistIndex);
			m_playManager->play();
			QVERIFY(plh->activeIndex() == CurrentPlaylistIndex);

			auto playlist = plh->playlist(CurrentPlaylistIndex);
			playlist->toggleStopAfterTrack(LastTrackBeforeStop);
			for(auto i = 1; i <= LastTrackBeforeStop; i++)
			{
				m_playManager->next();
				QCOMPARE(playlist->findCurrentTrackIndex(), i);
			}

			m_playManager->next();
			QCOMPARE(playlist->findCurrentTrackIndex(), -1);
		}
};

QTEST_GUILESS_MAIN(PlaylistHandlerTest)

#include "PlaylistHandlerTest.moc"
