import hashlib
import json
import logging
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Generator, List, Tuple

import pytest
from dateutil.tz import tzutc

from sublime_music.adapters import ConfigurationStore
from sublime_music.adapters.subsonic import SubsonicAdapter, api_objects as SubsonicAPI

MOCK_DATA_FILES = Path(__file__).parent.joinpath("mock_data")


@pytest.fixture
def adapter(tmp_path: Path):
    ConfigurationStore.MOCK = True
    config = ConfigurationStore(
        server_address="https://subsonic.example.com",
        username="test",
        verify_cert=True,
        salt_auth=False,
    )
    config.set_secret("password", "testpass")

    adapter = SubsonicAdapter(config, tmp_path)
    adapter._is_mock = True

    yield adapter
    adapter.shutdown()


@pytest.fixture
def salt_auth_adapter(tmp_path: Path):
    ConfigurationStore.MOCK = True
    config = ConfigurationStore(
        server_address="https://subsonic.example.com",
        username="test",
        verify_cert=True,
        salt_auth=True,
    )
    config.set_secret("password", "testpass")

    adapter = SubsonicAdapter(config, tmp_path)
    adapter._is_mock = True

    yield adapter
    adapter.shutdown()


def mock_data_files(request_name: str, mode: str = "r") -> Generator[Tuple[Path, Any], None, None]:
    """
    Yields all of the files, and each of the elements of in the file (separated by a
    line of ='s), for all files in the mock_data directory that start with
    ``request_name``. This only works for text such as JSON.
    """
    sep_re = re.compile(r"=+\n")

    num_files = 0
    for file in MOCK_DATA_FILES.iterdir():
        if file.name.split("-")[0] == request_name:
            with open(file, mode) as f:
                parts: List[str] = []
                aggregate: List[str] = []
                for line in f:
                    if sep_re.match(line):
                        parts.append("\n".join(aggregate))
                        aggregate = []
                        continue
                    aggregate.append(line)

                parts.append("\n".join(aggregate))
                print(file)  # noqa: T201
                num_files += 1
                yield file, iter(parts)

    # Make sure that is at least one test file
    assert num_files > 0


def mock_json(**obj: Any) -> str:
    return json.dumps({"subsonic-response": {"status": "ok", "version": "1.15.0", **obj}})


def camel_to_snake(name: str) -> str:
    name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
    return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()


def test_plain_auth_logic(adapter: SubsonicAdapter):
    expected = {
        "u": "test",
        "p": "testpass",
        "c": "Sublime Music",
        "f": "json",
        "v": "1.8.0",
    }
    assert sorted(expected.items()) == sorted(adapter._get_params().items())


def test_salt_auth_logic(salt_auth_adapter: SubsonicAdapter):
    expected = {"u": "test", "c": "Sublime Music", "f": "json", "v": "1.8.0"}

    params = salt_auth_adapter._get_params()
    assert "p" not in params
    assert "s" in params
    salt = params["s"]
    assert "t" in params
    assert params["t"] == hashlib.md5(f"testpass{salt}".encode()).hexdigest()
    assert all(key in params and params[key] == expected[key] for key in expected)


def test_migrate_configuration_populate_salt_auth():
    config = ConfigurationStore(
        server_address="https://subsonic.example.com",
        username="test",
        verify_cert=True,
    )
    SubsonicAdapter.migrate_configuration(config)
    assert "salt_auth" in config
    assert config["salt_auth"]


def test_migrate_configuration_salt_auth_present():
    config = ConfigurationStore(
        server_address="https://subsonic.example.com",
        username="test",
        verify_cert=True,
        salt_auth=False,
    )
    SubsonicAdapter.migrate_configuration(config)
    assert "salt_auth" in config
    assert not config["salt_auth"]


def test_make_url(adapter: SubsonicAdapter):
    assert adapter._make_url("foo") == "https://subsonic.example.com/rest/foo.view"


def test_ping_status(adapter: SubsonicAdapter):
    # Mock a connection error
    adapter._set_mock_data(Exception())
    assert not adapter.ping_status

    # Simulate some sort of ping error
    for filename, data in mock_data_files("ping_failed"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)
        assert not adapter.ping_status

    # Simulate valid ping
    adapter._set_mock_data(mock_json())
    # typing doesn't support multiprocessing.Value very well
    adapter._last_ping_timestamp.value = 0.0  # type: ignore
    adapter._set_ping_status()
    assert adapter.ping_status


def test_get_playlists(adapter: SubsonicAdapter):
    expected = [
        SubsonicAPI.Playlist(
            id="2",
            name="Test",
            song_count=132,
            duration=timedelta(seconds=33072),
            created=datetime(2020, 3, 27, 5, 38, 45, 0, tzinfo=tzutc()),
            changed=datetime(2020, 4, 9, 16, 3, 26, 0, tzinfo=tzutc()),
            comment="Foo",
            owner="foo",
            public=True,
            cover_art="pl-2",
        ),
        SubsonicAPI.Playlist(
            id="3",
            name="Bar",
            song_count=23,
            duration=timedelta(seconds=847),
            created=datetime(2020, 3, 27, 5, 39, 4, 0, tzinfo=tzutc()),
            changed=datetime(2020, 3, 27, 5, 45, 23, 0, tzinfo=tzutc()),
            comment="",
            owner="foo",
            public=False,
            cover_art="pl-3",
        ),
    ]

    for filename, data in mock_data_files("get_playlists"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)
        assert adapter.get_playlists() == sorted(expected, key=lambda e: e.name)

    # When playlists is null, expect an empty list.
    adapter._set_mock_data(mock_json())
    assert adapter.get_playlists() == []


def test_get_playlist_details(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("get_playlist_details"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        playlist_details = adapter.get_playlist_details("2")

        # Make sure that the song count is correct even if it's not provided.
        # Old versions of Subsonic don't have these properties.
        assert len(playlist_details.songs) == 2
        assert playlist_details.duration == timedelta(seconds=470)

        # Make sure that at least the first song got decoded properly.
        assert playlist_details.songs[0] == SubsonicAPI.Song(
            id="202",
            parent_id="318",
            title="What a Beautiful Name",
            _album="What a Beautiful Name - Single",
            album_id="48",
            _artist="Hillsong Worship",
            artist_id="38",
            track=1,
            year=2016,
            _genre="Christian & Gospel",
            cover_art="318",
            size=8381640,
            duration=timedelta(seconds=238),
            path="/".join(
                (
                    "Hillsong Worship",
                    "What a Beautiful Name - Single",
                    "01 What a Beautiful Name.m4a",
                )
            ),
            disc_number=1,
        )


def test_create_playlist(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("create_playlist"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        adapter.create_playlist(
            name="Foo",
            songs=[
                SubsonicAPI.Song(
                    id="202",
                    parent_id="318",
                    title="What a Beautiful Name",
                    _album="What a Beautiful Name - Single",
                    album_id="48",
                    _artist="Hillsong Worship",
                    artist_id="38",
                    track=1,
                    year=2016,
                    _genre="Christian & Gospel",
                    cover_art="318",
                    duration=timedelta(seconds=238),
                    path="/".join(
                        (
                            "Hillsong Worship",
                            "What a Beautiful Name - Single",
                            "01 What a Beautiful Name.m4a",
                        )
                    ),
                    disc_number=1,
                )
            ],
        )


def test_update_playlist(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("update_playlist"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        result_playlist = adapter.update_playlist(
            "1", name="Foo", comment="Bar", public=True, song_ids=["202"]
        )

        assert result_playlist.comment == "Bar"
        assert result_playlist.public is False


def test_get_song_details(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("get_song_details"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        song = adapter.get_song_details("1")
        assert (song.id, song.title, song.year, song.cover_art, song.duration) == (
            "1",
            "Sweet Caroline",
            2017,
            "544",
            timedelta(seconds=203),
        )
        assert song.path and song.path.endswith("Sweet Caroline.mp3")
        assert song.parent_id == "544"
        assert song.artist
        assert (song.artist.id, song.artist.name) == ("60", "Neil Diamond")
        assert song.album
        assert (song.album.id, song.album.name) == ("88", "50th Anniversary Collection")
        assert song.genre and song.genre.name == "Pop"


def test_get_song_details_missing_data(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("get_song_details_no_albumid"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        song = adapter.get_song_details("1")
        assert (song.id, song.title, song.year, song.cover_art, song.duration) == (
            "1",
            "Sweet Caroline",
            2017,
            "544",
            timedelta(seconds=203),
        )
        assert song.path and song.path.endswith("Sweet Caroline.mp3")
        assert song.parent_id == "544"
        assert song.artist
        assert (song.artist.id, song.artist.name) == ("60", "Neil Diamond")
        assert song.album
        assert (song.album.id, song.album.name) == (None, "50th Anniversary Collection")
        assert song.genre and song.genre.name == "Pop"

    for filename, data in mock_data_files("get_song_details_no_artistid"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        song = adapter.get_song_details("1")
        assert (song.id, song.title, song.year, song.cover_art, song.duration) == (
            "1",
            "Sweet Caroline",
            2017,
            "544",
            timedelta(seconds=203),
        )
        assert song.path and song.path.endswith("Sweet Caroline.mp3")
        assert song.parent_id == "544"
        assert song.artist
        assert (song.artist.id, song.artist.name) == (None, "Neil Diamond")
        assert song.album
        assert (song.album.id, song.album.name) == ("88", "50th Anniversary Collection")
        assert song.genre and song.genre.name == "Pop"


def test_get_genres(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("get_genres"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        genres = adapter.get_genres()

        assert len(genres) == 2
        assert [g.name for g in genres] == ["Country", "Pop"]


def test_get_artists(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("get_artists"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        artists = adapter.get_artists()
        assert len(artists) == 7
        assert {a.name for a in artists} == {
            "Adele",
            "Austin  French",
            "The Afters",
            "The Band Perry",
            "Basshunter",
            "Zac Brown Band",
            "Zach Williams",
        }


def test_get_ignored_articles(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("get_artists"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        ignored_articles = adapter.get_ignored_articles()
        assert ignored_articles == {"The", "El", "La", "Los", "Las", "Le", "Les"}


def test_get_ignored_articles_from_cached_get_artists(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("get_artists"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        adapter.get_artists()
        ignored_articles = adapter.get_ignored_articles()
        assert ignored_articles == {"The", "El", "La", "Los", "Las", "Le", "Les"}


def test_get_artist(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("get_artist"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        artist = adapter.get_artist("3")
        assert artist.album_count == 1
        assert artist.albums and len(artist.albums) == 1
        assert ("3", "Kane Brown") == (artist.albums[0].id, artist.albums[0].name)
        assert artist.artist_image_url == "ar-3"
        assert artist.biography and len(artist.biography) > 0
        assert artist.name == "Kane Brown"
        assert artist.similar_artists
        assert len(artist.similar_artists) == 20
        assert (first_similar := artist.similar_artists[0])
        assert first_similar
        assert first_similar.name == "Luke Combs"
        assert first_similar.artist_image_url == "ar-158"


def test_get_artist_with_good_image_url(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("get_artist_good_image_url"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        artist = adapter.get_artist("3")
        assert artist.album_count == 1
        assert artist.albums and len(artist.albums) == 1
        assert artist.biography and len(artist.biography) > 0
        assert artist.name == "Kane Brown"
        assert ("3", "Kane Brown") == (artist.albums[0].id, artist.albums[0].name)
        assert (
            artist.artist_image_url
            == "http://entertainermag.com/wp-content/uploads/2017/04/Kane-Brown-Web-Optimized.jpg"  # noqa: E501
        )


def test_get_play_queue(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("get_play_queue"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        play_queue = adapter.get_play_queue()
        assert play_queue
        assert play_queue.current_index and play_queue.current_index == 1
        assert play_queue.position == timedelta(milliseconds=98914)
        assert play_queue.username == "sumner"
        assert play_queue.changed == datetime(2020, 5, 12, 5, 16, 32, 114000, tzinfo=timezone.utc)
        assert play_queue.songs and len(play_queue.songs) == 5

        song = play_queue.songs[0]
        assert song.album and song.album.name == "Despacito"
        assert song.artist and song.artist.name == "Peter Bence"
        assert song.genre and song.genre.name == "Classical"


def test_get_album(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("get_album"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        album = adapter.get_album("243")
        assert (
            album.id,
            album.name,
            album.cover_art,
            album.song_count,
            album.year,
            album.duration,
        ) == (
            "243",
            "What You See Is What You Get",
            "al-243",
            17,
            2019,
            timedelta(seconds=3576),
        )
        assert album.artist
        assert (album.artist.id, album.artist.name) == ("158", "Luke Combs")
        assert album.genre and album.genre.name == "Country"
        assert album.songs
        assert len(album.songs) == 17
        assert [s.title for s in album.songs] == [
            "Beer Never Broke My Heart",
            "Refrigerator Door",
            "Even Though I'm Leaving",
            "Lovin' On You",
            "Moon Over Mexico",
            "1, 2 Many",
            "Blue Collar Boys",
            "New Every Day",
            "Reasons",
            "Every Little Bit Helps",
            "Dear Today",
            "What You See Is What You Get",
            "Does To Me",
            "Angels Workin' Overtime",
            "All Over Again",
            "Nothing Like You",
            "Better Together",
        ]


def test_get_music_directory(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("get_music_directory"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        directory = adapter.get_directory("3")
        assert directory.id == "60"
        assert directory.name == "Luke Bryan"
        assert directory.parent_id == "root"
        assert directory.children and len(directory.children) == 1
        child = directory.children[0]
        assert isinstance(child, SubsonicAPI.Directory)
        assert child.id == "542"
        assert child.name == "Crash My Party"
        assert child.parent_id == "60"

    for filename, data in mock_data_files("get_indexes"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        directory = adapter.get_directory("root")
        assert directory.id == "root"
        assert directory.parent_id is None
        assert len(directory.children) == 7
        child = directory.children[0]
        assert isinstance(child, SubsonicAPI.Directory)
        assert child.id == "73"
        assert child.name == "The Afters"
        assert child.parent_id == "root"


def test_search(adapter: SubsonicAdapter):
    for filename, data in mock_data_files("search3"):
        logging.info(filename)
        logging.debug(data)
        adapter._set_mock_data(data)

        search_results = adapter.search("3")
        assert len(search_results._songs) == 7
        assert len(search_results._artists) == 2
        assert len(search_results._albums) == 4
