"""
Tests are named 'test_nnn_xxxx' where 'nn n' is a number. This is used to ensure that
the tests run in order as we rely (in some cases) upon the results of an earlier test.
Yes, we shouldn't do that.
"""

# Standard library imports
import os
import shutil
import tempfile
import unittest
from unittest.mock import MagicMock, patch

# PyQt imports
from PyQt6.QtWidgets import QDialog, QFileDialog

# Third party imports
from mutagen.mp3 import MP3  # type: ignore
import pytest
from pytestqt.plugin import QtBot  # type: ignore

# App imports
from app import musicmuster
from app.models import (
    db,
    Playlists,
    Tracks,
)
from config import Config
from file_importer import FileImporter


# Custom fixture to adapt qtbot for use with unittest.TestCase
@pytest.fixture(scope="class")
def qtbot_adapter(qapp, request):
    """Adapt qtbot fixture for usefixtures and unittest.TestCase"""
    request.cls.qtbot = QtBot(request)


# Fixture for tmp_path to be available in the class
@pytest.fixture(scope="class")
def class_tmp_path(request, tmp_path_factory):
    """Provide a class-wide tmp_path"""
    request.cls.tmp_path = tmp_path_factory.mktemp("pytest_tmp")


@pytest.mark.usefixtures("qtbot_adapter", "class_tmp_path")
class MyTestCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        """Runs once before any test in this class"""

        db.create_all()

        cls.widget = musicmuster.Window()

        # Create a playlist for all tests
        playlist_name = "file importer playlist"
        with db.Session() as session:
            playlist = Playlists(session=session, name=playlist_name, template_id=0)
            cls.widget._open_playlist(playlist)

        # Create our musicstore
        cls.import_source = tempfile.mkdtemp(suffix="_MMsource_pytest", dir="/tmp")
        Config.REPLACE_FILES_DEFAULT_SOURCE = cls.import_source
        cls.musicstore = tempfile.mkdtemp(suffix="_MMstore_pytest", dir="/tmp")
        Config.IMPORT_DESTINATION = cls.musicstore

    @classmethod
    def tearDownClass(cls):
        """Runs once after all tests"""

        db.drop_all()
        shutil.rmtree(cls.musicstore)
        shutil.rmtree(cls.import_source)

    def setUp(self):
        """Runs before each test"""

        with self.qtbot.waitExposed(self.widget):
            self.widget.show()

    def tearDown(self):
        """Runs after each test"""
        self.widget.close()  # Close UI to prevent side effects

    def wait_for_workers(self, timeout: int = 10000):
        """
        Let import threads workers run to completion
        """

        def workers_empty():
            assert FileImporter.workers == {}

        self.qtbot.waitUntil(workers_empty, timeout=timeout)

    def test_001_import_no_files(self):
        """Try importing with no files to import"""

        with patch("file_importer.show_OK") as mock_show_ok:
            self.widget.import_files_wrapper()
            mock_show_ok.assert_called_once_with(
                "File import",
                f"No files in {Config.REPLACE_FILES_DEFAULT_SOURCE} to import",
                None,
            )

    def test_002_import_file_and_cancel(self):
        """Cancel file import"""

        test_track_path = "testdata/isa.mp3"
        shutil.copy(test_track_path, self.import_source)

        with (
            patch("file_importer.PickMatch") as MockPickMatch,
            patch("file_importer.show_OK") as mock_show_ok,
        ):
            # Create a mock instance of PickMatch
            mock_dialog_instance = MagicMock()
            MockPickMatch.return_value = mock_dialog_instance

            # Simulate the user clicking OK in the dialog
            mock_dialog_instance.exec.return_value = QDialog.DialogCode.Rejected
            mock_dialog_instance.selected_track_id = -1  # Simulated return value

            self.widget.import_files_wrapper()

            # Ensure PickMatch was instantiated correctly
            MockPickMatch.assert_called_once_with(
                new_track_description="I'm So Afraid (Fleetwood Mac)",
                choices=[("Do not import", -1, ""), ("Import as new track", 0, "")],
                default=1,
            )

            # Verify exec() was called
            mock_dialog_instance.exec.assert_called_once()

            # Ensure selected_track_id was accessed after dialog.exec()
            assert mock_dialog_instance.selected_track_id < 0

            mock_show_ok.assert_called_once_with(
                "File not imported",
                "isa.mp3 will not be imported because you asked not to import this file",
            )

    def test_003_import_first_file(self):
        """Import file into empty directory"""

        test_track_path = "testdata/isa.mp3"
        shutil.copy(test_track_path, self.import_source)

        with patch("file_importer.PickMatch") as MockPickMatch:
            # Create a mock instance of PickMatch
            mock_dialog_instance = MagicMock()
            MockPickMatch.return_value = mock_dialog_instance

            # Simulate the user clicking OK in the dialog
            mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted
            mock_dialog_instance.selected_track_id = 0  # Simulated return value

            self.widget.import_files_wrapper()

            # Ensure PickMatch was instantiated correctly
            MockPickMatch.assert_called_once_with(
                new_track_description="I'm So Afraid (Fleetwood Mac)",
                choices=[("Do not import", -1, ""), ("Import as new track", 0, "")],
                default=1,
            )

            # Verify exec() was called
            mock_dialog_instance.exec.assert_called_once()

            # Ensure selected_track_id was accessed after dialog.exec()
            assert mock_dialog_instance.selected_track_id == 0

        self.wait_for_workers()

        # Check track was imported
        with db.Session() as session:
            tracks = Tracks.get_all(session)
            assert len(tracks) == 1
            track = tracks[0]
            assert track.title == "I'm So Afraid"
            assert track.artist == "Fleetwood Mac"
            track_file = os.path.join(
                self.musicstore, os.path.basename(test_track_path)
            )
            assert track.path == track_file
            assert os.path.exists(track_file)
            assert os.listdir(self.import_source) == []

    def test_004_import_second_file(self):
        """Import a second file"""

        test_track_path = "testdata/lovecats.mp3"
        shutil.copy(test_track_path, self.import_source)

        with patch("file_importer.PickMatch") as MockPickMatch:
            # Create a mock instance of PickMatch
            mock_dialog_instance = MagicMock()
            MockPickMatch.return_value = mock_dialog_instance

            # Simulate the user clicking OK in the dialog
            mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted
            mock_dialog_instance.selected_track_id = 0  # Simulated return value

            self.widget.import_files_wrapper()

            # Ensure PickMatch was instantiated correctly
            MockPickMatch.assert_called_once_with(
                new_track_description="The Lovecats (The Cure)",
                choices=[("Do not import", -1, ""), ("Import as new track", 0, "")],
                default=1,
            )

            # Verify exec() was called
            mock_dialog_instance.exec.assert_called_once()

            # Ensure selected_track_id was accessed after dialog.exec()
            assert mock_dialog_instance.selected_track_id == 0

        self.wait_for_workers()

        # Check track was imported
        with db.Session() as session:
            tracks = Tracks.get_all(session)
            assert len(tracks) == 2
            track = tracks[1]
            assert track.title == "The Lovecats"
            assert track.artist == "The Cure"
            track_file = os.path.join(
                self.musicstore, os.path.basename(test_track_path)
            )
            assert track.path == track_file
            assert os.path.exists(track_file)
            assert os.listdir(self.import_source) == []

    def test_005_replace_file(self):
        """Import the same file again and update existing track"""

        test_track_path = "testdata/lovecats.mp3"
        shutil.copy(test_track_path, self.import_source)

        with patch("file_importer.PickMatch") as MockPickMatch:
            # Create a mock instance of PickMatch
            mock_dialog_instance = MagicMock()
            MockPickMatch.return_value = mock_dialog_instance

            # Simulate the user clicking OK in the dialog
            mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted
            mock_dialog_instance.selected_track_id = 2  # Simulated return value

            self.widget.import_files_wrapper()

            # Ensure PickMatch was instantiated correctly
            MockPickMatch.assert_called_once_with(
                new_track_description="The Lovecats (The Cure)",
                choices=[
                    ("Do not import", -1, ""),
                    ("Import as new track", 0, ""),
                    (
                        "The Lovecats (The Cure) (100%)",
                        2,
                        os.path.join(
                            self.musicstore, os.path.basename(test_track_path)
                        ),
                    ),
                ],
                default=2,
            )

            # Verify exec() was called
            mock_dialog_instance.exec.assert_called_once()

        self.wait_for_workers()

        # Check track was imported
        with db.Session() as session:
            tracks = Tracks.get_all(session)
            assert len(tracks) == 2
            track = tracks[1]
            assert track.title == "The Lovecats"
            assert track.artist == "The Cure"
            assert track.id == 2
            track_file = os.path.join(
                self.musicstore, os.path.basename(test_track_path)
            )
            assert track.path == track_file
            assert os.path.exists(track_file)
            assert os.listdir(self.import_source) == []

    def test_006_import_file_no_tags(self) -> None:
        """Try to import untagged file"""

        test_track_path = "testdata/lovecats.mp3"
        test_filename = os.path.basename(test_track_path)

        shutil.copy(test_track_path, self.import_source)
        import_file = os.path.join(self.import_source, test_filename)
        assert os.path.exists(import_file)

        # Remove tags
        src = MP3(import_file)
        src.delete()
        src.save()

        with patch("file_importer.show_OK") as mock_show_ok:
            self.widget.import_files_wrapper()
            mock_show_ok.assert_called_once_with(
                "File not imported",
                f"{test_filename} will not be imported because of tag errors "
                f"(Missing tags in {import_file})",
            )

    def test_007_import_unreadable_file(self) -> None:
        """Import unreadable file"""

        test_track_path = "testdata/lovecats.mp3"
        test_filename = os.path.basename(test_track_path)

        shutil.copy(test_track_path, self.import_source)
        import_file = os.path.join(self.import_source, test_filename)
        assert os.path.exists(import_file)

        # Make undreadable
        os.chmod(import_file, 0)

        with patch("file_importer.show_OK") as mock_show_ok:
            self.widget.import_files_wrapper()
            mock_show_ok.assert_called_once_with(
                "File not imported",
                f"{test_filename} will not be imported because {import_file} is unreadable",
            )

        # clean up
        os.chmod(import_file, 0o777)
        os.unlink(import_file)

    def test_008_import_new_file_existing_destination(self) -> None:
        """Import duplicate file"""

        test_track_path = "testdata/lovecats.mp3"
        test_filename = os.path.basename(test_track_path)
        new_destination = os.path.join(self.musicstore, "lc2.mp3")

        shutil.copy(test_track_path, self.import_source)
        import_file = os.path.join(self.import_source, test_filename)
        assert os.path.exists(import_file)

        with (
            patch("file_importer.PickMatch") as MockPickMatch,
            patch.object(
                QFileDialog, "getSaveFileName", return_value=(new_destination, "")
            ) as mock_file_dialog,
            patch("file_importer.show_OK") as mock_show_ok,
        ):
            mock_file_dialog.return_value = (
                new_destination,
                "",
            )  # Ensure mock correctly returns expected value

            # Create a mock instance of PickMatch
            mock_dialog_instance = MagicMock()
            MockPickMatch.return_value = mock_dialog_instance

            # Simulate the user clicking OK in the dialog
            mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted
            mock_dialog_instance.selected_track_id = 0  # Simulated return value

            self.widget.import_files_wrapper()

            # Ensure PickMatch was instantiated correctly
            MockPickMatch.assert_called_once_with(
                new_track_description="The Lovecats (The Cure)",
                choices=[
                    ("Do not import", -1, ""),
                    ("Import as new track", 0, ""),
                    (
                        "The Lovecats (The Cure) (100%)",
                        2,
                        os.path.join(
                            self.musicstore, os.path.basename(test_track_path)
                        ),
                    ),
                ],
                default=2,
            )

            # Verify exec() was called
            mock_dialog_instance.exec.assert_called_once()

            destination = os.path.join(self.musicstore, test_filename)
            mock_show_ok.assert_called_once_with(
                title="Desintation path exists",
                msg=f"New import requested but default destination path ({destination}) "
                "already exists. Click OK and choose where to save this track",
                parent=None,
            )

            self.wait_for_workers()

            # Ensure QFileDialog was called and returned expected value
            assert mock_file_dialog.called  # Ensure the mock was used
            result = mock_file_dialog()
            assert result[0] == new_destination  # Validate return value

        # Check track was imported
        with db.Session() as session:
            tracks = Tracks.get_all(session)
            assert len(tracks) == 3
            track = tracks[2]
            assert track.title == "The Lovecats"
            assert track.artist == "The Cure"
            assert track.id == 3
            assert track.path == new_destination
            assert os.path.exists(new_destination)
            assert os.listdir(self.import_source) == []

            # Remove file so as not to interfere with later tests
            session.delete(track)
            tracks = Tracks.get_all(session)
            assert len(tracks) == 2
            session.commit()

            os.unlink(new_destination)
            assert not os.path.exists(new_destination)

    def test_009_import_similar_file(self) -> None:
        """Import file with similar, but different, title"""

        test_track_path = "testdata/lovecats.mp3"
        test_filename = os.path.basename(test_track_path)

        shutil.copy(test_track_path, self.import_source)
        import_file = os.path.join(self.import_source, test_filename)
        assert os.path.exists(import_file)

        # Change title tag
        src = MP3(import_file)
        src["TIT2"].text[0] += " xyz"
        src.save()

        with patch("file_importer.PickMatch") as MockPickMatch:
            # Create a mock instance of PickMatch
            mock_dialog_instance = MagicMock()
            MockPickMatch.return_value = mock_dialog_instance

            # Simulate the user clicking OK in the dialog
            mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted
            mock_dialog_instance.selected_track_id = 2  # Simulated return value

            self.widget.import_files_wrapper()

            # Ensure PickMatch was instantiated correctly
            MockPickMatch.assert_called_once_with(
                new_track_description="The Lovecats xyz (The Cure)",
                choices=[
                    ("Do not import", -1, ""),
                    ("Import as new track", 0, ""),
                    (
                        "The Lovecats (The Cure) (93%)",
                        2,
                        os.path.join(
                            self.musicstore, os.path.basename(test_track_path)
                        ),
                    ),
                ],
                default=2,
            )

            # Verify exec() was called
            mock_dialog_instance.exec.assert_called_once()

        self.wait_for_workers()

        # Check track was imported
        with db.Session() as session:
            tracks = Tracks.get_all(session)
            assert len(tracks) == 2
            track = tracks[1]
            assert track.title == "The Lovecats xyz"
            assert track.artist == "The Cure"
            assert track.id == 2
            track_file = os.path.join(
                self.musicstore, os.path.basename(test_track_path)
            )
            assert track.path == track_file
            assert os.path.exists(track_file)
            assert os.listdir(self.import_source) == []
