from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch

import pytest
import time_machine
from agents.models import AgentSession
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone
from music.models import Album, Artist, Track
from podcasts.models import PodcastEpisode
from scrobbles.models import Scrobble, ShareViewLog
from scrobbles.sqids import encode_scrobble_share
from tasks.models import Task
from videos.models import Channel, Video
from workouts.models import Exercise, WorkoutRoutine


@pytest.mark.django_db
def test_get_not_allowed_from_mopidy(client, valid_auth_token):
    url = reverse("scrobbles:mopidy-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    response = client.get(url, headers=headers)
    assert response.status_code == 405


@pytest.mark.django_db
def test_get_not_allowed_from_jellyfin(client, valid_auth_token):
    url = reverse("scrobbles:jellyfin-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    response = client.get(url, headers=headers)
    assert response.status_code == 405


@pytest.mark.django_db
def test_bad_mopidy_request_data(client, valid_auth_token):
    url = reverse("scrobbles:mopidy-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    response = client.post(
        url, "not valid json", content_type="application/json", headers=headers
    )
    assert response.status_code == 400
    assert (
        response.data["detail"]
        == "JSON parse error - Expecting value: line 1 column 1 (char 0)"
    )


@pytest.mark.django_db
def test_bad_jellyfin_request_data(client, valid_auth_token):
    url = reverse("scrobbles:jellyfin-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    response = client.post(
        url, "not valid json", content_type="application/json", headers=headers
    )
    assert response.status_code == 400
    assert (
        response.data["detail"]
        == "JSON parse error - Expecting value: line 1 column 1 (char 0)"
    )


@pytest.mark.django_db
@patch("music.models.Artist.find_or_create")
@patch("music.models.Track.find_or_create")
def test_create_scrobble_from_mopidy_track_webhook(
    mock_track_fc,
    mock_artist_fc,
    client,
    valid_auth_token,
    mopidy_track,
):
    mock_artist = MagicMock(spec=Artist)
    mock_artist.id = 1
    mock_artist_fc.return_value = mock_artist

    mock_track = MagicMock(spec=Track)
    mock_track.id = 1
    mock_track.scrobble_for_user.return_value = Scrobble(
        id=1, track_id=1, user_id=1, in_progress=True
    )
    mock_track_fc.return_value = mock_track

    url = reverse("scrobbles:mopidy-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    response = client.post(
        url,
        mopidy_track.request_data,
        content_type="application/json",
        headers=headers,
    )
    assert response.status_code == 200
    assert response.data == {"scrobble_id": 1}
    mock_track.scrobble_for_user.assert_called_once()


@pytest.mark.django_db
@patch("music.models.Artist.find_or_create")
@patch("music.models.Track.find_or_create")
def test_create_scrobble_from_jellyfin_track_webhook(
    mock_track_fc,
    mock_artist_fc,
    client,
    valid_auth_token,
    jellyfin_track,
):
    mock_artist = MagicMock(spec=Artist)
    mock_artist.id = 1
    mock_artist_fc.return_value = mock_artist

    mock_track = MagicMock(spec=Track)
    mock_track.id = 1
    mock_track.scrobble_for_user.return_value = Scrobble(
        id=1, track_id=1, user_id=1, in_progress=True
    )
    mock_track_fc.return_value = mock_track

    url = reverse("scrobbles:jellyfin-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}

    with time_machine.travel(datetime(2024, 1, 14, 12, 00, 1)):
        jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        response = client.post(
            url,
            jellyfin_track.request_json,
            content_type="application/json",
            headers=headers,
        )
        assert response.status_code == 200
        assert response.data == {"scrobble_id": 1}
        mock_track.scrobble_for_user.assert_called_once()


@pytest.mark.django_db
@patch("music.models.Artist.find_or_create")
@patch("music.models.Track.find_or_create")
def test_mopidy_track_webhook_creates_track_and_scrobble(
    mock_track_fc,
    mock_artist_fc,
    client,
    valid_auth_token,
    mopidy_track,
):
    artist = Artist.objects.create(name="Sublime")
    album = Album.objects.create(name="Sublime", album_artist=artist)
    track = Track.objects.create(
        title="Same in the End",
        artist_fk=artist,
        base_run_time_seconds=60,
    )
    track.artists.add(artist)
    track.albums.add(album)

    mock_artist_fc.return_value = artist
    mock_track_fc.return_value = track

    url = reverse("scrobbles:mopidy-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    response = client.post(
        url,
        mopidy_track.request_data,
        content_type="application/json",
        headers=headers,
    )
    assert response.status_code == 200
    assert response.data == {"scrobble_id": 1}

    scrobble = Scrobble.objects.get(id=1)
    assert scrobble.track == track
    assert scrobble.source == "Mopidy"


@pytest.mark.django_db
@patch("music.models.Artist.find_or_create")
@patch("music.models.Track.find_or_create")
def test_jellyfin_track_webhook_creates_track_and_scrobble(
    mock_track_fc,
    mock_artist_fc,
    client,
    valid_auth_token,
    jellyfin_track,
):
    artist = Artist.objects.create(name="Carly Rae Jepsen")
    album = Album.objects.create(name="Emotion", album_artist=artist)
    track = Track.objects.create(
        title="Emotion",
        artist_fk=artist,
        base_run_time_seconds=60,
    )
    track.artists.add(artist)
    track.albums.add(album)

    mock_artist_fc.return_value = artist
    mock_track_fc.return_value = track

    url = reverse("scrobbles:jellyfin-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}

    with time_machine.travel(datetime(2024, 1, 14, 12, 00, 1)):
        jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        response = client.post(
            url,
            jellyfin_track.request_json,
            content_type="application/json",
            headers=headers,
        )
        assert response.status_code == 200
        assert response.data == {"scrobble_id": 1}

        scrobble = Scrobble.objects.get(id=1)
        assert scrobble.track == track
        assert scrobble.source == "Jellyfin"
        assert "raw_data" in scrobble.log


@pytest.mark.django_db
@patch("music.models.Artist.find_or_create")
@patch("music.models.Track.find_or_create")
def test_mopidy_track_webhook_stores_raw_data(
    mock_track_fc,
    mock_artist_fc,
    client,
    valid_auth_token,
    mopidy_track,
):
    artist = Artist.objects.create(name="Sublime")
    album = Album.objects.create(name="Sublime", album_artist=artist)
    track = Track.objects.create(
        title="Same in the End",
        artist_fk=artist,
        base_run_time_seconds=60,
    )
    track.artists.add(artist)
    track.albums.add(album)

    mock_artist_fc.return_value = artist
    mock_track_fc.return_value = track

    url = reverse("scrobbles:mopidy-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    response = client.post(
        url,
        mopidy_track.request_data,
        content_type="application/json",
        headers=headers,
    )
    assert response.status_code == 200
    assert response.data == {"scrobble_id": 1}

    scrobble = Scrobble.objects.get(id=1)
    assert scrobble.track == track
    assert scrobble.source == "Mopidy"
    assert "raw_data" in scrobble.log
    assert scrobble.log["raw_data"]["name"] == "Same in the End"


@pytest.mark.django_db
@patch("music.models.Artist.find_or_create")
@patch("music.models.Track.find_or_create")
def test_mopidy_track_webhook_stores_album_id(
    mock_track_fc,
    mock_artist_fc,
    client,
    valid_auth_token,
    mopidy_track,
):
    artist = Artist.objects.create(name="Sublime")
    album = Album.objects.create(name="Sublime", album_artist=artist)
    track = Track.objects.create(
        title="Same in the End",
        artist_fk=artist,
        album=album,
        base_run_time_seconds=60,
    )
    track.artists.add(artist)

    mock_artist_fc.return_value = artist
    mock_track_fc.return_value = track

    url = reverse("scrobbles:mopidy-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    response = client.post(
        url,
        mopidy_track.request_data,
        content_type="application/json",
        headers=headers,
    )
    assert response.status_code == 200

    scrobble = Scrobble.objects.get(id=1)
    assert "album_id" in scrobble.log
    assert scrobble.log["album_id"] == album.id
    assert "album" not in scrobble.log


@pytest.mark.django_db
@patch("music.models.Artist.find_or_create")
@patch("music.models.Track.find_or_create")
def test_jellyfin_track_webhook_stores_raw_data(
    mock_track_fc,
    mock_artist_fc,
    client,
    valid_auth_token,
    jellyfin_track,
):
    artist = Artist.objects.create(name="Carly Rae Jepsen")
    album = Album.objects.create(name="Emotion", album_artist=artist)
    track = Track.objects.create(
        title="Emotion",
        artist_fk=artist,
        base_run_time_seconds=60,
    )
    track.artists.add(artist)
    track.albums.add(album)

    mock_artist_fc.return_value = artist
    mock_track_fc.return_value = track

    url = reverse("scrobbles:jellyfin-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}

    with time_machine.travel(datetime(2024, 1, 14, 12, 00, 1)):
        jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        response = client.post(
            url,
            jellyfin_track.request_json,
            content_type="application/json",
            headers=headers,
        )
        assert response.status_code == 200

        scrobble = Scrobble.objects.get(id=1)
        assert scrobble.track == track
        assert scrobble.source == "Jellyfin"
        assert "raw_data" in scrobble.log
        assert scrobble.log["raw_data"]["Name"] == "Emotion"


@pytest.mark.django_db
@patch("music.models.Artist.find_or_create")
@patch("music.models.Track.find_or_create")
def test_jellyfin_track_webhook_stores_album_id(
    mock_track_fc,
    mock_artist_fc,
    client,
    valid_auth_token,
    jellyfin_track,
):
    artist = Artist.objects.create(name="Carly Rae Jepsen")
    album = Album.objects.create(name="Emotion", album_artist=artist)
    track = Track.objects.create(
        title="Emotion",
        artist_fk=artist,
        album=album,
        base_run_time_seconds=60,
    )
    track.artists.add(artist)

    mock_artist_fc.return_value = artist
    mock_track_fc.return_value = track

    url = reverse("scrobbles:jellyfin-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}

    with time_machine.travel(datetime(2024, 1, 14, 12, 00, 1)):
        jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        response = client.post(
            url,
            jellyfin_track.request_json,
            content_type="application/json",
            headers=headers,
        )
        assert response.status_code == 200

        scrobble = Scrobble.objects.get(id=1)
        assert "album_id" in scrobble.log
        assert scrobble.log["album_id"] == album.id


@pytest.mark.skip("Need to refactor")
@pytest.mark.django_db
@patch("music.utils.lookup_artist_from_mb", return_value={})
@patch(
    "music.utils.lookup_album_dict_from_mb",
    return_value={"year": "1999", "mb_group_id": 1},
)
@patch("music.utils.lookup_track_from_mb", return_value={})
@patch("music.models.lookup_artist_from_tadb", return_value={})
@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"})
@patch("music.models.Album.fetch_artwork", return_value=None)
@patch("music.models.Album.scrape_allmusic", return_value=None)
def test_scrobble_mopidy_same_track_different_album(
    mock_lookup_artist,
    mock_lookup_album,
    mock_lookup_track,
    mock_lookup_artist_tadb,
    mock_lookup_album_tadb,
    mock_fetch_artwork,
    mock_scrape_allmusic,
    client,
    mopidy_track,
    mopidy_track_diff_album_request_data,
    valid_auth_token,
):
    url = reverse("scrobbles:mopidy-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    response = client.post(
        url,
        mopidy_track.request_data,
        content_type="application/json",
        headers=headers,
    )
    assert response.status_code == 200
    assert response.data == {"scrobble_id": 1}
    scrobble = Scrobble.objects.last()
    assert scrobble.media_obj.album.name == "Sublime"

    response = client.post(
        url,
        mopidy_track_diff_album_request_data,
        content_type="application/json",
    )

    assert response.status_code == 200
    assert response.data == {"scrobble_id": 2}
    scrobble = Scrobble.objects.last()
    assert scrobble.media_obj.__class__ == Track
    assert scrobble.media_obj.album.name == "Sublime"
    assert scrobble.media_obj.title == "Same in the End"


@pytest.mark.django_db
@patch(
    "podcasts.sources.podcastindex.lookup_podcast_from_podcastindex",
    return_value={},
)
def test_scrobble_mopidy_podcast(
    mock_lookup_podcast, client, mopidy_podcast_request_data, valid_auth_token
):
    url = reverse("scrobbles:mopidy-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    response = client.post(
        url,
        mopidy_podcast_request_data,
        content_type="application/json",
        headers=headers,
    )
    assert response.status_code == 200
    assert response.data == {"scrobble_id": 1}

    scrobble = Scrobble.objects.get(id=1)
    assert scrobble.media_obj.__class__ == PodcastEpisode
    assert scrobble.media_obj.title == "Up First"


@pytest.mark.skip("Need to refactor")
@pytest.mark.django_db
@patch("music.utils.lookup_artist_from_mb", return_value={})
@patch(
    "music.utils.lookup_album_dict_from_mb",
    return_value={"year": "1999", "mb_group_id": 1},
)
@patch("music.utils.lookup_track_from_mb", return_value={})
@patch("music.models.lookup_artist_from_tadb", return_value={})
@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"})
@patch("music.models.Album.fetch_artwork", return_value=None)
@patch("music.models.Album.scrape_allmusic", return_value=None)
def test_scrobble_jellyfin_track(
    mock_lookup_artist,
    mock_lookup_album,
    mock_lookup_track,
    mock_lookup_artist_tadb,
    mock_lookup_album_tadb,
    mock_fetch_artwork,
    mock_scrape_allmusic,
    client,
    jellyfin_track,
    valid_auth_token,
):
    url = reverse("scrobbles:jellyfin-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}

    with time_machine.travel(datetime(2024, 1, 14, 12, 00, 1)):
        jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        response = client.post(
            url,
            jellyfin_track.request_json,
            content_type="application/json",
            headers=headers,
        )
        assert response.status_code == 200
        assert response.data == {"scrobble_id": 1}

    scrobble = Scrobble.objects.get(id=1)
    assert scrobble.media_obj.__class__ == Track
    assert scrobble.media_obj.title == "Emotion"


@pytest.mark.django_db
def test_scrobble_detail_view_with_notes_as_flat_list(client):
    user = get_user_model().objects.create_user(
        username="testuser", email="test@example.com", password="testpass"
    )
    task = Task.objects.create(title="Test Task", description="Test description")
    scrobble = Scrobble.objects.create(
        task=task,
        media_type="Task",
        user=user,
        visibility="public",
        log={
            "notes": ["First note", "Second note"],
            "description": "Test description",
        },
    )
    url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})
    response = client.get(url)
    assert response.status_code == 200
    assert "First note" in response.content.decode()
    assert "Second note" in response.content.decode()


@pytest.mark.django_db
def test_scrobble_detail_view_with_notes_as_dict_timestamps(client):
    user = get_user_model().objects.create_user(
        username="testuser", email="test@example.com", password="testpass"
    )
    task = Task.objects.create(title="Test Task", description="Test description")
    scrobble = Scrobble.objects.create(
        task=task,
        media_type="Task",
        user=user,
        visibility="public",
        log={
            "notes": [
                {"2024-01-01 10:00:00": "Note at first timestamp"},
                {"2024-01-02 11:30:00": "Note at second timestamp"},
            ],
            "description": "Test description",
        },
    )
    url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})
    response = client.get(url)
    assert response.status_code == 200
    content = response.content.decode()
    assert "2024-01-01 10:00:00" in content
    assert "Note at first timestamp" in content
    assert "2024-01-02 11:30:00" in content
    assert "Note at second timestamp" in content


@pytest.mark.django_db
def test_scrobble_detail_view_with_notes_and_labels(client):
    user = get_user_model().objects.create_user(
        username="testuser", email="test@example.com", password="testpass"
    )
    task = Task.objects.create(title="Test Task", description="Test description")
    from django.utils import timezone
    scrobble = Scrobble.objects.create(
        task=task,
        media_type="Task",
        user=user,
        visibility="public",
        timestamp=timezone.now(),
        log={
            "notes": [
                {"2024-01-01 10:00:00": "Note with label"},
            ],
            "labels": ["work", "urgent"],
            "description": "Test description",
        },
    )
    client.login(username="testuser", password="testpass")
    url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})
    response = client.get(url)
    assert response.status_code == 200
    content = response.content.decode()
    assert "work" in content
    assert "urgent" in content


@pytest.mark.django_db
def test_scrobble_detail_view_post_updates_log(client):
    user = get_user_model().objects.create_user(
        username="testuser", email="test@example.com", password="testpass"
    )
    task = Task.objects.create(title="Test Task", description="Test description")
    scrobble = Scrobble.objects.create(
        task=task,
        media_type="Task",
        user=user,
        log={
            "notes": ["Original note"],
            "description": "Original description",
        },
    )
    url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})

    client.force_login(user)
    response = client.post(
        url,
        {
            "description": "Updated description",
            "notes": "Updated note",
        },
    )
    assert response.status_code == 302

    scrobble.refresh_from_db()
    assert scrobble.log["description"] == "Updated description"
    assert isinstance(scrobble.log["notes"], dict)
    assert list(scrobble.log["notes"].values()) == ["Updated note"]


@pytest.mark.skip("Need to refactor")
@pytest.mark.django_db
@patch("music.utils.lookup_artist_from_mb", return_value={})
@patch(
    "music.utils.lookup_album_dict_from_mb",
    return_value={"year": "1999", "mb_group_id": 1},
)
@patch("music.utils.lookup_track_from_mb", return_value={})
@patch("music.models.lookup_artist_from_tadb", return_value={})
@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"})
@patch("music.models.Album.fetch_artwork", return_value=None)
@patch("music.models.Album.scrape_allmusic", return_value=None)
def test_scrobble_jellyfin_track_update(
    mock_lookup_artist,
    mock_lookup_album,
    mock_lookup_track,
    mock_lookup_artist_tadb,
    mock_lookup_album_tadb,
    mock_fetch_artwork,
    mock_scrape_allmusic,
    test_track,
    client,
    jellyfin_track,
    valid_auth_token,
):
    Scrobble.objects.create(
        timestamp=timezone.now() - timedelta(minutes=0.5),
        track=Track.objects.first(),
        user_id=1,
    )
    url = reverse("scrobbles:jellyfin-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}

    jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
        "%Y-%m-%d %H:%M:%S"
    )
    response = client.post(
        url,
        jellyfin_track.request_json,
        content_type="application/json",
        headers=headers,
    )

    assert response.status_code == 200
    assert response.data == {"scrobble_id": 1}

    scrobble = Scrobble.objects.get(id=1)
    assert scrobble.media_obj.__class__ == Track
    assert scrobble.media_obj.title == "Emotion"


@pytest.mark.skip("Need to refactor")
@pytest.mark.django_db
@patch("music.utils.lookup_artist_from_mb", return_value={})
@patch(
    "music.utils.lookup_album_dict_from_mb",
    return_value={"year": "1999", "mb_group_id": 1},
)
@patch("music.utils.lookup_track_from_mb", return_value={})
@patch("music.models.lookup_artist_from_tadb", return_value={})
@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"})
@patch("music.models.Album.fetch_artwork", return_value=None)
@patch("music.models.Album.scrape_allmusic", return_value=None)
def test_scrobble_jellyfin_track_create_new(
    mock_lookup_artist,
    mock_lookup_album,
    mock_lookup_track,
    mock_lookup_artist_tadb,
    mock_lookup_album_tadb,
    mock_fetch_artwork,
    mock_scrape_allmusic,
    test_track,
    client,
    jellyfin_track,
    valid_auth_token,
):
    url = reverse("scrobbles:jellyfin-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    Scrobble.objects.create(
        timestamp=timezone.now() - timedelta(minutes=1),
        track=Track.objects.first(),
        user_id=1,
    )
    jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
        "%Y-%m-%d %H:%M:%S"
    )
    response = client.post(
        url,
        jellyfin_track.request_json,
        content_type="application/json",
        headers=headers,
    )

    assert response.status_code == 200
    assert response.data == {"scrobble_id": 2}

    scrobble = Scrobble.objects.get(id=1)
    assert scrobble.media_obj.__class__ == Track
    assert scrobble.media_obj.title == "Emotion"


@pytest.mark.django_db
def test_get_not_allowed_from_gps(client, valid_auth_token):
    url = reverse("scrobbles:gps-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    response = client.get(url, headers=headers)
    assert response.status_code == 405


@pytest.mark.django_db
def test_gps_webhook_creates_location(client, valid_auth_token):
    url = reverse("scrobbles:gps-webhook")
    headers = {"Authorization": f"Token {valid_auth_token}"}
    gps_data = {
        "lat": "40.7128",
        "lon": "-74.0060",
        "alt": "10.5",
        "time": "2024-01-14T12:00:00Z",
        "prov": "gps",
    }
    response = client.post(
        url,
        gps_data,
        content_type="application/json",
        headers=headers,
    )
    assert response.status_code == 200
    assert "scrobble_id" in response.data


@pytest.mark.django_db
def test_share_view_shared_visibility(client):
    user = get_user_model().objects.create_user(
        username="shareuser", password="testpass"
    )
    task = Task.objects.create(title="Test Task")
    scrobble = Scrobble.objects.create(
        task=task, media_type="Task", user=user, visibility="shared",
        timestamp=timezone.now(),
    )
    url = reverse(
        "scrobbles:shared-detail",
        kwargs={"sqid": encode_scrobble_share(scrobble.id, scrobble.share_token_version)},
    )
    response = client.get(url)
    assert response.status_code == 200


@pytest.mark.django_db
def test_share_view_public_visibility(client):
    user = get_user_model().objects.create_user(
        username="shareuser2", password="testpass"
    )
    task = Task.objects.create(title="Test Task")
    scrobble = Scrobble.objects.create(
        task=task, media_type="Task", user=user, visibility="public",
        timestamp=timezone.now(),
    )
    url = reverse(
        "scrobbles:shared-detail",
        kwargs={"sqid": encode_scrobble_share(scrobble.id, scrobble.share_token_version)},
    )
    response = client.get(url)
    assert response.status_code == 200


@pytest.mark.django_db
def test_share_view_private_visibility_returns_404(client):
    user = get_user_model().objects.create_user(
        username="shareuser3", password="testpass"
    )
    task = Task.objects.create(title="Test Task")
    scrobble = Scrobble.objects.create(
        task=task, media_type="Task", user=user, visibility="private",
        timestamp=timezone.now(),
    )
    url = reverse(
        "scrobbles:shared-detail",
        kwargs={"sqid": encode_scrobble_share(scrobble.id, scrobble.share_token_version)},
    )
    response = client.get(url)
    assert response.status_code == 404


@pytest.mark.django_db
def test_share_view_invalid_sqid_returns_404(client):
    url = reverse("scrobbles:shared-detail", kwargs={"sqid": "InvalidSqid123"})
    response = client.get(url)
    assert response.status_code == 404


@pytest.mark.django_db
def test_share_view_expired_token_returns_404(client):
    user = get_user_model().objects.create_user(
        username="shareuser4", password="testpass"
    )
    task = Task.objects.create(title="Test Task")
    scrobble = Scrobble.objects.create(
        task=task, media_type="Task", user=user, visibility="shared",
        timestamp=timezone.now(),
    )
    old_sqid = encode_scrobble_share(scrobble.id, scrobble.share_token_version)
    scrobble.regenerate_share_token()
    url = reverse("scrobbles:shared-detail", kwargs={"sqid": old_sqid})
    response = client.get(url)
    assert response.status_code == 404


@pytest.mark.django_db
def test_share_view_increments_count_and_logs_view(client):
    user = get_user_model().objects.create_user(
        username="shareuser5", password="testpass"
    )
    task = Task.objects.create(title="Test Task")
    scrobble = Scrobble.objects.create(
        task=task, media_type="Task", user=user, visibility="shared",
        timestamp=timezone.now(),
    )
    assert scrobble.share_view_count == 0

    url = reverse(
        "scrobbles:shared-detail",
        kwargs={"sqid": encode_scrobble_share(scrobble.id, scrobble.share_token_version)},
    )
    response = client.get(url)
    assert response.status_code == 200

    scrobble.refresh_from_db()
    assert scrobble.share_view_count == 1
    assert ShareViewLog.objects.filter(scrobble=scrobble).count() == 1

    log_entry = ShareViewLog.objects.filter(scrobble=scrobble).first()
    assert log_entry.ip_address == "127.0.0.1"
    assert log_entry.user_agent == ""
    assert log_entry.referrer == ""


@pytest.mark.django_db
def test_share_view_shows_agent_session_logdata(client):
    user = get_user_model().objects.create_user(
        username="shareagentsession", password="testpass"
    )
    agent_session, _ = AgentSession.find_or_create(
        provider="opencode", model="opencode"
    )
    scrobble = Scrobble.objects.create(
        agent_session=agent_session,
        media_type="AgentSession",
        user=user,
        visibility="shared",
        timestamp=timezone.now(),
        log={
            "title": "Hello agent",
            "provider": "opencode",
            "model": "opencode",
            "turns": [
                {
                    "prompt_id": "abc-1",
                    "prompt": "What is the capital of France?",
                    "response": "Paris",
                }
            ],
        },
    )
    url = reverse(
        "scrobbles:shared-detail",
        kwargs={"sqid": encode_scrobble_share(scrobble.id, scrobble.share_token_version)},
    )
    response = client.get(url)
    assert response.status_code == 200
    content = response.content.decode()
    assert "What is the capital of France?" in content
    assert "Paris" in content
    # Shared page must not expose the owner's follow-up/retry UI
    assert "Ask a follow-up" not in content
    assert "/follow-up/" not in content
    assert "Retry" not in content


@pytest.mark.django_db
def test_share_view_strips_sensitive_logdata(client):
    user = get_user_model().objects.create_user(
        username="sharesensitive", password="testpass"
    )
    task = Task.objects.create(title="Weigh-in")
    scrobble = Scrobble.objects.create(
        task=task,
        media_type="Task",
        user=user,
        visibility="shared",
        timestamp=timezone.now(),
        log={
            "weight": 82.5,
            "body_fat": 18.0,
            "waist": 88.0,
            "comment": "feeling good",
        },
    )
    url = reverse(
        "scrobbles:shared-detail",
        kwargs={"sqid": encode_scrobble_share(scrobble.id, scrobble.share_token_version)},
    )
    response = client.get(url)
    assert response.status_code == 200
    content = response.content.decode()
    assert "82.5" in content
    assert "feeling good" in content
    assert "18.0" not in content
    assert "88.0" not in content


@pytest.mark.django_db
def test_share_view_owner_logdata_unchanged():
    user = get_user_model().objects.create_user(
        username="ownerlogdata", password="testpass"
    )
    task = Task.objects.create(title="Weigh-in")
    scrobble = Scrobble.objects.create(
        task=task,
        media_type="Task",
        user=user,
        visibility="shared",
        timestamp=timezone.now(),
        log={
            "weight": 82.5,
            "body_fat": 18.0,
        },
    )
    assert scrobble.log["body_fat"] == 18.0
    assert "body_fat" not in scrobble.public_log
    assert scrobble.public_log["weight"] == 82.5


@pytest.mark.django_db
def test_share_view_shows_workout_logdata(client):
    user = get_user_model().objects.create_user(
        username="shareworkout", password="testpass"
    )
    exercise = Exercise.objects.create(name="Deadlift")
    routine = WorkoutRoutine.objects.create(title="Test Routine")
    scrobble = Scrobble.objects.create(
        user=user,
        workout_routine=routine,
        media_type=Scrobble.MediaType.WORKOUT,
        visibility="shared",
        timestamp=timezone.now(),
        log={
            "workouts": [
                {"exercise_id": exercise.id, "sets": 3, "reps": 10, "weight_kg": 50.0}
            ],
            "duration_minutes": 45,
            "bodyweight_kg": 80.0,
        },
    )
    url = reverse(
        "scrobbles:shared-detail",
        kwargs={"sqid": encode_scrobble_share(scrobble.id, scrobble.share_token_version)},
    )
    response = client.get(url)
    assert response.status_code == 200
    content = response.content.decode()
    assert "Workout Details" in content
    assert "Deadlift" in content
    assert "3 x 10" in content
    assert "80.0" not in content


@pytest.mark.django_db
def test_explore_view_shows_only_public_scrobbles(client):
    user = get_user_model().objects.create_user(
        username="exploreuser", password="testpass"
    )
    public_task = Task.objects.create(title="Public Task Title")
    shared_task = Task.objects.create(title="Shared Task Title")
    private_task = Task.objects.create(title="Private Task Title")
    ts = timezone.now()
    public_scrobble = Scrobble.objects.create(
        task=public_task, media_type="Task", user=user, visibility="public",
        timestamp=ts,
    )
    Scrobble.objects.create(
        task=shared_task, media_type="Task", user=user, visibility="shared",
        timestamp=ts,
    )
    Scrobble.objects.create(
        task=private_task, media_type="Task", user=user, visibility="private",
        timestamp=ts,
    )

    url = reverse("scrobbles:explore")
    response = client.get(url)
    assert response.status_code == 200

    content = response.content.decode()
    assert "Public Task Title" in content
    assert "Shared Task Title" not in content
    assert "Private Task Title" not in content


@pytest.mark.django_db
def test_change_visibility_owner_can_change(client):
    user = get_user_model().objects.create_user(
        username="visuser", password="testpass"
    )
    task = Task.objects.create(title="Test Task")
    scrobble = Scrobble.objects.create(
        task=task, media_type="Task", user=user, visibility="private",
        timestamp=timezone.now(),
    )

    client.force_login(user)
    url = reverse("scrobbles:change-visibility", kwargs={"pk": scrobble.id})
    response = client.post(url, {"visibility": "shared"})
    assert response.status_code == 302

    scrobble.refresh_from_db()
    assert scrobble.visibility == "shared"


@pytest.mark.django_db
def test_change_visibility_non_owner_gets_404(client):
    owner = get_user_model().objects.create_user(
        username="owner", password="testpass"
    )
    other = get_user_model().objects.create_user(
        username="other", password="testpass"
    )
    task = Task.objects.create(title="Test Task")
    scrobble = Scrobble.objects.create(
        task=task, media_type="Task", user=owner, visibility="private",
        timestamp=timezone.now(),
    )

    client.force_login(other)
    url = reverse("scrobbles:change-visibility", kwargs={"pk": scrobble.id})
    response = client.post(url, {"visibility": "shared"})
    assert response.status_code == 404

    scrobble.refresh_from_db()
    assert scrobble.visibility == "private"


@pytest.mark.django_db
def test_change_visibility_anonymous_redirects_to_login(client):
    user = get_user_model().objects.create_user(
        username="anontest", password="testpass"
    )
    task = Task.objects.create(title="Test Task")
    scrobble = Scrobble.objects.create(
        task=task, media_type="Task", user=user, visibility="private",
        timestamp=timezone.now(),
    )
    url = reverse("scrobbles:change-visibility", kwargs={"pk": scrobble.id})
    response = client.post(url, {"visibility": "shared"})
    assert response.status_code == 302
    assert "/login/" in response.url


@pytest.mark.django_db
def test_regenerate_share_token_invalidates_old_sqid(client):
    user = get_user_model().objects.create_user(
        username="regentest", password="testpass"
    )
    task = Task.objects.create(title="Test Task")
    scrobble = Scrobble.objects.create(
        task=task, media_type="Task", user=user, visibility="shared",
        timestamp=timezone.now(),
    )
    old_sqid = encode_scrobble_share(scrobble.id, scrobble.share_token_version)

    client.force_login(user)
    url = reverse("scrobbles:regenerate-share-token", kwargs={"pk": scrobble.id})
    response = client.post(url)
    assert response.status_code == 302

    scrobble.refresh_from_db()
    assert scrobble.share_token_version == 1

    old_url = reverse("scrobbles:shared-detail", kwargs={"sqid": old_sqid})
    old_response = client.get(old_url)
    assert old_response.status_code == 404

    new_sqid = encode_scrobble_share(scrobble.id, scrobble.share_token_version)
    new_url = reverse("scrobbles:shared-detail", kwargs={"sqid": new_sqid})
    new_response = client.get(new_url)
    assert new_response.status_code == 200


@pytest.mark.django_db
def test_share_analytics_owner_can_view(client):
    user = get_user_model().objects.create_user(
        username="analyticsuser", password="testpass"
    )
    task = Task.objects.create(title="Test Task")
    scrobble = Scrobble.objects.create(
        task=task, media_type="Task", user=user, visibility="shared",
        timestamp=timezone.now(),
    )

    client.force_login(user)
    url = reverse("scrobbles:share-analytics", kwargs={"pk": scrobble.id})
    response = client.get(url)
    assert response.status_code == 200


@pytest.mark.django_db
def test_share_analytics_non_owner_gets_404(client):
    owner = get_user_model().objects.create_user(
        username="analyticsowner", password="testpass"
    )
    other = get_user_model().objects.create_user(
        username="analyticsother", password="testpass"
    )
    task = Task.objects.create(title="Test Task")
    scrobble = Scrobble.objects.create(
        task=task, media_type="Task", user=owner, visibility="shared",
        timestamp=timezone.now(),
    )

    client.force_login(other)
    url = reverse("scrobbles:share-analytics", kwargs={"pk": scrobble.id})
    response = client.get(url)
    assert response.status_code == 404


@pytest.mark.django_db
def test_share_analytics_shows_view_logs(client):
    user = get_user_model().objects.create_user(
        username="analyticsviews", password="testpass"
    )
    task = Task.objects.create(title="Test Task")
    scrobble = Scrobble.objects.create(
        task=task, media_type="Task", user=user, visibility="shared",
        timestamp=timezone.now(),
    )

    sqid = encode_scrobble_share(scrobble.id, scrobble.share_token_version)
    share_url = reverse("scrobbles:shared-detail", kwargs={"sqid": sqid})
    client.get(share_url)
    client.get(share_url)

    client.force_login(user)
    url = reverse("scrobbles:share-analytics", kwargs={"pk": scrobble.id})
    response = client.get(url)
    assert response.status_code == 200
    content = response.content.decode()
    assert "127.0.0.1" in content
    assert ShareViewLog.objects.filter(scrobble=scrobble).count() == 2


@pytest.mark.django_db
def test_scrobble_start_with_channel_uuid_creates_scrobble(client, valid_auth_token):
    channel = Channel.objects.create(name="Test Channel")
    url = reverse("scrobbles:start", kwargs={"media_uuid": channel.uuid})
    headers = {"Authorization": f"Token {valid_auth_token}", "Referer": "/scrobbles/"}
    response = client.get(url, headers=headers)
    assert response.status_code == 302
    user = get_user_model().objects.get(email="test@exmaple.com")
    assert Scrobble.objects.filter(channel=channel, user=user).exists()


@pytest.mark.django_db
def test_scrobble_start_with_video_uuid_creates_scrobble(client, valid_auth_token):
    video = Video.objects.create(title="Test Video")
    url = reverse("scrobbles:start", kwargs={"media_uuid": video.uuid})
    headers = {"Authorization": f"Token {valid_auth_token}", "Referer": "/scrobbles/"}
    response = client.get(url, headers=headers)
    assert response.status_code == 302
    user = get_user_model().objects.get(email="test@exmaple.com")
    assert Scrobble.objects.filter(video=video, user=user).exists()


@pytest.mark.django_db
def test_long_plays_view_does_not_crash_on_unknown_app_labels(client):
    user = get_user_model().objects.create_user(
        username="longplays", password="testpass"
    )
    client.force_login(user)
    url = reverse("scrobbles:long-plays")
    response = client.get(url)
    assert response.status_code == 200
