package playlist

import (
	"os"
	"path/filepath"
	"testing"
)

func TestSanitizeFilename(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		input    string
		expected string
	}{
		{
			name:     "simple name",
			input:    "My Playlist",
			expected: "my-playlist",
		},
		{
			name:     "special characters",
			input:    "Rock & Roll: Best Of!",
			expected: "rock-roll-best-of",
		},
		{
			name:     "unicode characters",
			input:    "Música Favorita",
			expected: "música-favorita",
		},
		{
			name:     "empty string",
			input:    "",
			expected: "playlist",
		},
		{
			name:     "only special characters",
			input:    "!!!@@@###",
			expected: "playlist",
		},
		{
			name:     "underscores become hyphens",
			input:    "my_cool_playlist",
			expected: "my-cool-playlist",
		},
		{
			name:     "multiple spaces collapse",
			input:    "my   big   playlist",
			expected: "my-big-playlist",
		},
		{
			name:     "leading and trailing hyphens trimmed",
			input:    "  --hello--  ",
			expected: "hello",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			result := sanitizeFilename(tt.input)
			if result != tt.expected {
				t.Errorf(
					"sanitizeFilename(%q) = %q, want %q",
					tt.input, result, tt.expected,
				)
			}
		})
	}
}

func TestPlaylistFilePath(t *testing.T) {
	t.Parallel()

	result := playlistFilePath("/data/playlists", 42, "My Favorites")
	expected := filepath.Join(
		"/data/playlists", "42-my-favorites.m3u8",
	)

	if result != expected {
		t.Errorf(
			"playlistFilePath() = %q, want %q",
			result, expected,
		)
	}
}

func TestWriteAndParseM3U8(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()

	entries := []m3uEntry{
		{
			RelativePath: "Artist/Album/01 - Song.flac",
			DurationSec:  243,
			DisplayTitle: "Artist - Song",
		},
		{
			RelativePath: "Other/Track.mp3",
			DurationSec:  180,
			DisplayTitle: "Other - Track",
		},
	}

	err := writeM3U8(dir, 1, "Test Playlist", entries)
	if err != nil {
		t.Fatalf("writeM3U8() error = %v", err)
	}

	// Verify file exists.
	expectedPath := filepath.Join(dir, "1-test-playlist.m3u8")
	if _, err := os.Stat(expectedPath); err != nil {
		t.Fatalf("expected file %q to exist: %v", expectedPath, err)
	}

	// Parse it back.
	parsed, err := parseM3U8(expectedPath)
	if err != nil {
		t.Fatalf("parseM3U8() error = %v", err)
	}

	if parsed.Name != "Test Playlist" {
		t.Errorf(
			"parsed.Name = %q, want %q",
			parsed.Name, "Test Playlist",
		)
	}

	if len(parsed.Entries) != len(entries) {
		t.Fatalf(
			"parsed %d entries, want %d",
			len(parsed.Entries), len(entries),
		)
	}

	for i, entry := range parsed.Entries {
		if entry.RelativePath != entries[i].RelativePath {
			t.Errorf(
				"entry[%d].RelativePath = %q, want %q",
				i, entry.RelativePath,
				entries[i].RelativePath,
			)
		}

		if entry.DurationSec != entries[i].DurationSec {
			t.Errorf(
				"entry[%d].DurationSec = %d, want %d",
				i, entry.DurationSec,
				entries[i].DurationSec,
			)
		}

		if entry.DisplayTitle != entries[i].DisplayTitle {
			t.Errorf(
				"entry[%d].DisplayTitle = %q, want %q",
				i, entry.DisplayTitle,
				entries[i].DisplayTitle,
			)
		}
	}
}

func TestWriteM3U8EmptyPlaylist(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()

	err := writeM3U8(dir, 5, "Empty", nil)
	if err != nil {
		t.Fatalf("writeM3U8() error = %v", err)
	}

	parsed, err := parseM3U8(
		filepath.Join(dir, "5-empty.m3u8"),
	)
	if err != nil {
		t.Fatalf("parseM3U8() error = %v", err)
	}

	if parsed.Name != "Empty" {
		t.Errorf("parsed.Name = %q, want %q", parsed.Name, "Empty")
	}

	if len(parsed.Entries) != 0 {
		t.Errorf(
			"parsed %d entries, want 0",
			len(parsed.Entries),
		)
	}
}

func TestWriteM3U8EmptyDir(t *testing.T) {
	t.Parallel()

	err := writeM3U8("", 1, "test", nil)
	if err == nil {
		t.Fatal("expected error for empty dir path")
	}
}

func TestParseM3U8EmptyFile(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	emptyFile := filepath.Join(dir, "empty.m3u8")

	err := os.WriteFile(
		emptyFile,
		[]byte(""),
		0o644,
	)
	if err != nil {
		t.Fatalf("could not write test file: %v", err)
	}

	_, err = parseM3U8(emptyFile)
	if err == nil {
		t.Fatal("expected error for empty M3U file")
	}
}

func TestParseM3U8NonExistentFile(t *testing.T) {
	t.Parallel()

	_, err := parseM3U8("/nonexistent/file.m3u8")
	if err == nil {
		t.Fatal("expected error for non-existent file")
	}
}

func TestToRelativePath(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name        string
		absPath     string
		libraryRoot string
		expected    string
	}{
		{
			name:        "normal relative",
			absPath:     "/music/Artist/Album/song.flac",
			libraryRoot: "/music",
			expected:    "Artist/Album/song.flac",
		},
		{
			name:        "path outside library root",
			absPath:     "/other/song.flac",
			libraryRoot: "/music",
			expected:    "/other/song.flac",
		},
		{
			name:        "empty library root",
			absPath:     "/music/song.flac",
			libraryRoot: "",
			expected:    "/music/song.flac",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			result := toRelativePath(
				tt.absPath, tt.libraryRoot,
			)
			if result != tt.expected {
				t.Errorf(
					"toRelativePath(%q, %q) = %q, want %q",
					tt.absPath, tt.libraryRoot,
					result, tt.expected,
				)
			}
		})
	}
}

func TestToAbsolutePath(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name        string
		relPath     string
		libraryRoot string
		expected    string
	}{
		{
			name:        "relative path",
			relPath:     "Artist/Album/song.flac",
			libraryRoot: "/music",
			expected:    "/music/Artist/Album/song.flac",
		},
		{
			name:        "already absolute",
			relPath:     "/music/song.flac",
			libraryRoot: "/other",
			expected:    "/music/song.flac",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			result := toAbsolutePath(
				tt.relPath, tt.libraryRoot,
			)
			if result != tt.expected {
				t.Errorf(
					"toAbsolutePath(%q, %q) = %q, want %q",
					tt.relPath, tt.libraryRoot,
					result, tt.expected,
				)
			}
		})
	}
}

func TestDisplayTitle(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		artist   string
		title    string
		expected string
	}{
		{
			name:     "both present",
			artist:   "Artist",
			title:    "Title",
			expected: "Artist - Title",
		},
		{
			name:     "artist only",
			artist:   "Artist",
			title:    "",
			expected: "Artist",
		},
		{
			name:     "title only",
			artist:   "",
			title:    "Title",
			expected: "Title",
		},
		{
			name:     "neither present",
			artist:   "",
			title:    "",
			expected: "Unknown",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			result := displayTitle(tt.artist, tt.title)
			if result != tt.expected {
				t.Errorf(
					"displayTitle(%q, %q) = %q, want %q",
					tt.artist, tt.title,
					result, tt.expected,
				)
			}
		})
	}
}

func TestExtractPlaylistID(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		filePath string
		expected int64
	}{
		{
			name:     "normal ID-prefixed filename",
			filePath: "/data/playlists/42-my-favorites.m3u8",
			expected: 42,
		},
		{
			name:     "no ID prefix",
			filePath: "/data/playlists/my-favorites.m3u8",
			expected: 0,
		},
		{
			name:     "ID only",
			filePath: "/data/playlists/1-.m3u8",
			expected: 1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			result := extractPlaylistID(tt.filePath)
			if result != tt.expected {
				t.Errorf(
					"extractPlaylistID(%q) = %d, want %d",
					tt.filePath, result, tt.expected,
				)
			}
		})
	}
}

func TestFindPlaylistFile(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()

	// Create a playlist file.
	err := writeM3U8(dir, 7, "Test", nil)
	if err != nil {
		t.Fatalf("writeM3U8() error = %v", err)
	}

	// Find it.
	found, err := findPlaylistFile(dir, 7)
	if err != nil {
		t.Fatalf("findPlaylistFile() error = %v", err)
	}

	if found == "" {
		t.Fatal("expected to find playlist file")
	}

	// Try to find a non-existent ID.
	found, err = findPlaylistFile(dir, 999)
	if err != nil {
		t.Fatalf("findPlaylistFile() error = %v", err)
	}

	if found != "" {
		t.Errorf("expected empty string, got %q", found)
	}
}

func TestIsValidM3UExtension(t *testing.T) {
	t.Parallel()

	tests := []struct {
		ext      string
		expected bool
	}{
		{".m3u", true},
		{".m3u8", true},
		{".M3U", true},
		{".M3U8", true},
		{".mp3", false},
		{".txt", false},
		{"", false},
	}

	for _, tt := range tests {
		t.Run(tt.ext, func(t *testing.T) {
			t.Parallel()

			result := isValidM3UExtension(tt.ext)
			if result != tt.expected {
				t.Errorf(
					"isValidM3UExtension(%q) = %v, want %v",
					tt.ext, result, tt.expected,
				)
			}
		})
	}
}

func TestRemoveOldPlaylistFile(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()

	// Create an initial playlist file.
	err := writeM3U8(dir, 3, "Old Name", nil)
	if err != nil {
		t.Fatalf("writeM3U8() error = %v", err)
	}

	oldPath := filepath.Join(dir, "3-old-name.m3u8")
	if _, err := os.Stat(oldPath); err != nil {
		t.Fatalf("old file should exist: %v", err)
	}

	// Write with a new name — should remove the old file.
	err = writeM3U8(dir, 3, "New Name", nil)
	if err != nil {
		t.Fatalf("writeM3U8() error = %v", err)
	}

	// Old file should be gone.
	if _, err := os.Stat(oldPath); !os.IsNotExist(err) {
		t.Error("old file should have been removed")
	}

	// New file should exist.
	newPath := filepath.Join(dir, "3-new-name.m3u8")
	if _, err := os.Stat(newPath); err != nil {
		t.Errorf("new file should exist: %v", err)
	}
}

func TestListPlaylistFiles(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()

	// Create some playlist files.
	for i := int64(1); i <= 3; i++ {
		if err := writeM3U8(
			dir, i, "playlist", nil,
		); err != nil {
			t.Fatalf("writeM3U8() error = %v", err)
		}
	}

	// Also create a non-m3u8 file that should be ignored.
	err := os.WriteFile(
		filepath.Join(dir, "notes.txt"),
		[]byte("test"),
		0o644,
	)
	if err != nil {
		t.Fatalf("could not create decoy file: %v", err)
	}

	files, err := listPlaylistFiles(dir)
	if err != nil {
		t.Fatalf("listPlaylistFiles() error = %v", err)
	}

	if len(files) != 3 {
		t.Errorf("found %d files, want 3", len(files))
	}
}

func TestParseExtInf(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name         string
		line         string
		expectedDur  int
		expectedName string
	}{
		{
			name:         "standard EXTINF",
			line:         "#EXTINF:243,Artist - Title",
			expectedDur:  243,
			expectedName: "Artist - Title",
		},
		{
			name:         "duration only",
			line:         "#EXTINF:180",
			expectedDur:  180,
			expectedName: "",
		},
		{
			name:         "zero duration",
			line:         "#EXTINF:0,Some Title",
			expectedDur:  0,
			expectedName: "Some Title",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			dur, title := parseExtInf(tt.line)
			if dur != tt.expectedDur {
				t.Errorf(
					"duration = %d, want %d",
					dur, tt.expectedDur,
				)
			}

			if title != tt.expectedName {
				t.Errorf(
					"title = %q, want %q",
					title, tt.expectedName,
				)
			}
		})
	}
}

func TestFindPlaylistFileOverlappingIDs(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()

	// Create playlists with IDs 1 and 10 — the glob
	// pattern "1-*.m3u8" must not match "10-longer.m3u8".
	if err := writeM3U8(dir, 1, "Short", nil); err != nil {
		t.Fatalf("writeM3U8(1) error = %v", err)
	}

	if err := writeM3U8(dir, 10, "Longer", nil); err != nil {
		t.Fatalf("writeM3U8(10) error = %v", err)
	}

	found, err := findPlaylistFile(dir, 1)
	if err != nil {
		t.Fatalf("findPlaylistFile(1) error = %v", err)
	}

	if got := extractPlaylistID(found); got != 1 {
		t.Errorf(
			"findPlaylistFile(1) returned ID %d, want 1",
			got,
		)
	}

	found, err = findPlaylistFile(dir, 10)
	if err != nil {
		t.Fatalf("findPlaylistFile(10) error = %v", err)
	}

	if got := extractPlaylistID(found); got != 10 {
		t.Errorf(
			"findPlaylistFile(10) returned ID %d, want 10",
			got,
		)
	}
}

func TestParseM3U8SimpleFormat(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	simpleFile := filepath.Join(dir, "simple.m3u")

	// Write a simple M3U with no #EXTM3U header — just paths.
	content := "Artist/Album/01 - Song.flac\nOther/Track.mp3\n"

	if err := os.WriteFile(
		simpleFile, []byte(content), 0o644,
	); err != nil {
		t.Fatalf("could not write test file: %v", err)
	}

	parsed, err := parseM3U8(simpleFile)
	if err != nil {
		t.Fatalf("parseM3U8() error = %v", err)
	}

	if len(parsed.Entries) != 2 {
		t.Fatalf(
			"parsed %d entries, want 2",
			len(parsed.Entries),
		)
	}

	if parsed.Entries[0].RelativePath !=
		"Artist/Album/01 - Song.flac" {
		t.Errorf(
			"entry[0].RelativePath = %q, want %q",
			parsed.Entries[0].RelativePath,
			"Artist/Album/01 - Song.flac",
		)
	}

	if parsed.Entries[1].RelativePath !=
		"Other/Track.mp3" {
		t.Errorf(
			"entry[1].RelativePath = %q, want %q",
			parsed.Entries[1].RelativePath,
			"Other/Track.mp3",
		)
	}
}

func TestParseM3U8SimpleFormatWithComments(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	simpleFile := filepath.Join(dir, "commented.m3u")

	// Simple M3U with comment lines (no #EXTM3U header).
	content := "# Generated by SomeApp\n" +
		"Artist/Song.flac\n" +
		"# Another comment\n" +
		"Other/Track.mp3\n"

	if err := os.WriteFile(
		simpleFile, []byte(content), 0o644,
	); err != nil {
		t.Fatalf("could not write test file: %v", err)
	}

	parsed, err := parseM3U8(simpleFile)
	if err != nil {
		t.Fatalf("parseM3U8() error = %v", err)
	}

	if len(parsed.Entries) != 2 {
		t.Fatalf(
			"parsed %d entries, want 2",
			len(parsed.Entries),
		)
	}

	if parsed.Entries[0].RelativePath !=
		"Artist/Song.flac" {
		t.Errorf(
			"entry[0].RelativePath = %q, want %q",
			parsed.Entries[0].RelativePath,
			"Artist/Song.flac",
		)
	}
}

func TestRemoveM3UEntries(t *testing.T) {
	t.Parallel()

	entries := []m3uEntry{
		{RelativePath: "Artist/Song1.flac"},
		{RelativePath: "Artist/Song2.flac"},
		{RelativePath: "Artist/Song3.flac"},
	}

	targets := map[string]struct{}{
		"/music/Artist/Song2.flac": {},
	}

	result := removeM3UEntries(
		entries, targets, []string{"/music"},
	)

	if len(result) != 2 {
		t.Fatalf("expected 2 entries, got %d", len(result))
	}

	if result[0].RelativePath != "Artist/Song1.flac" {
		t.Errorf(
			"entry[0] = %q, want %q",
			result[0].RelativePath,
			"Artist/Song1.flac",
		)
	}

	if result[1].RelativePath != "Artist/Song3.flac" {
		t.Errorf(
			"entry[1] = %q, want %q",
			result[1].RelativePath,
			"Artist/Song3.flac",
		)
	}
}

func TestRemoveM3UEntriesAll(t *testing.T) {
	t.Parallel()

	entries := []m3uEntry{
		{RelativePath: "Song.flac"},
	}

	targets := map[string]struct{}{
		"/music/Song.flac": {},
	}

	result := removeM3UEntries(
		entries, targets, []string{"/music"},
	)

	if len(result) != 0 {
		t.Errorf("expected 0 entries, got %d", len(result))
	}
}

func TestReplaceM3UEntryPaths(t *testing.T) {
	t.Parallel()

	entries := []m3uEntry{
		{
			RelativePath: "old/path/song.flac",
			DurationSec:  180,
			DisplayTitle: "Song",
		},
		{
			RelativePath: "other/track.mp3",
			DurationSec:  240,
			DisplayTitle: "Track",
		},
	}

	replacements := map[string]string{
		"/music/old/path/song.flac": "new/path/song.flac",
	}

	result := replaceM3UEntryPaths(
		entries, replacements, []string{"/music"},
	)

	if len(result) != 2 {
		t.Fatalf("expected 2 entries, got %d", len(result))
	}

	if result[0].RelativePath != "new/path/song.flac" {
		t.Errorf(
			"entry[0].RelativePath = %q, want %q",
			result[0].RelativePath,
			"new/path/song.flac",
		)
	}

	// Duration and title should be preserved.
	if result[0].DurationSec != 180 {
		t.Errorf(
			"entry[0].DurationSec = %d, want 180",
			result[0].DurationSec,
		)
	}

	// Unchanged entry should remain the same.
	if result[1].RelativePath != "other/track.mp3" {
		t.Errorf(
			"entry[1].RelativePath = %q, want %q",
			result[1].RelativePath,
			"other/track.mp3",
		)
	}
}

func TestFindM3UEntry(t *testing.T) {
	t.Parallel()

	entries := []m3uEntry{
		{RelativePath: "Artist/Song1.flac"},
		{RelativePath: "Artist/Song2.flac"},
		{RelativePath: "Artist/Song3.flac"},
	}

	entry, idx := findM3UEntry(
		entries, "/music/Artist/Song2.flac",
		[]string{"/music"},
	)

	if idx != 1 {
		t.Errorf("expected index 1, got %d", idx)
	}

	if entry.RelativePath != "Artist/Song2.flac" {
		t.Errorf(
			"entry.RelativePath = %q, want %q",
			entry.RelativePath,
			"Artist/Song2.flac",
		)
	}

	// Not found.
	_, idx = findM3UEntry(
		entries, "/music/Artist/Missing.flac",
		[]string{"/music"},
	)

	if idx != -1 {
		t.Errorf("expected index -1, got %d", idx)
	}
}

func TestResolveM3UPath(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name         string
		relativePath string
		libraryRoots []string
		knownPaths   map[string]struct{}
		expected     string
	}{
		{
			name:         "absolute path returned as-is",
			relativePath: "/music/song.flac",
			libraryRoots: []string{"/other"},
			knownPaths:   nil,
			expected:     "/music/song.flac",
		},
		{
			name:         "relative path resolves against first root",
			relativePath: "Artist/song.flac",
			libraryRoots: []string{"/music", "/archive"},
			knownPaths: map[string]struct{}{
				"/music/Artist/song.flac": {},
			},
			expected: "/music/Artist/song.flac",
		},
		{
			name:         "relative path resolves against second root",
			relativePath: "Artist/song.flac",
			libraryRoots: []string{"/music", "/archive"},
			knownPaths: map[string]struct{}{
				"/archive/Artist/song.flac": {},
			},
			expected: "/archive/Artist/song.flac",
		},
		{
			name:         "no match falls back to first root",
			relativePath: "Artist/song.flac",
			libraryRoots: []string{"/music", "/archive"},
			knownPaths:   map[string]struct{}{},
			expected:     "/music/Artist/song.flac",
		},
		{
			name:         "empty roots returns path unchanged",
			relativePath: "Artist/song.flac",
			libraryRoots: nil,
			knownPaths:   nil,
			expected:     "Artist/song.flac",
		},
		{
			name:         "nil knownPaths falls back to first root",
			relativePath: "Artist/song.flac",
			libraryRoots: []string{"/music"},
			knownPaths:   nil,
			expected:     "/music/Artist/song.flac",
		},
		{
			name:         "absolute path in knownPaths returned",
			relativePath: "/music/Artist/song.flac",
			libraryRoots: []string{"/other"},
			knownPaths: map[string]struct{}{
				"/music/Artist/song.flac": {},
			},
			expected: "/music/Artist/song.flac",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			result := resolveM3UPath(
				tt.relativePath,
				tt.libraryRoots,
				tt.knownPaths,
			)
			if result != tt.expected {
				t.Errorf(
					"resolveM3UPath(%q, %v, ...) = %q, want %q",
					tt.relativePath,
					tt.libraryRoots,
					result, tt.expected,
				)
			}
		})
	}
}

func TestToRelativePathMultiRoot(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name         string
		absolutePath string
		libraryRoots []string
		expected     string
	}{
		{
			name:         "path under first root",
			absolutePath: "/music/Artist/song.flac",
			libraryRoots: []string{"/music", "/archive"},
			expected:     "Artist/song.flac",
		},
		{
			name:         "path under second root",
			absolutePath: "/archive/Artist/song.flac",
			libraryRoots: []string{"/music", "/archive"},
			expected:     "Artist/song.flac",
		},
		{
			name:         "path under no root returns absolute",
			absolutePath: "/other/song.flac",
			libraryRoots: []string{"/music", "/archive"},
			expected:     "/other/song.flac",
		},
		{
			name:         "empty roots returns absolute",
			absolutePath: "/music/song.flac",
			libraryRoots: nil,
			expected:     "/music/song.flac",
		},
		{
			name:         "empty root string skipped",
			absolutePath: "/music/song.flac",
			libraryRoots: []string{"", "/music"},
			expected:     "song.flac",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			result := toRelativePathMultiRoot(
				tt.absolutePath, tt.libraryRoots,
			)
			if result != tt.expected {
				t.Errorf(
					"toRelativePathMultiRoot(%q, %v) = %q, want %q",
					tt.absolutePath,
					tt.libraryRoots,
					result, tt.expected,
				)
			}
		})
	}
}

func TestRemoveM3UEntriesMultiRoot(t *testing.T) {
	t.Parallel()

	entries := []m3uEntry{
		{RelativePath: "Artist/Song1.flac"},
		{RelativePath: "Band/Song2.flac"},
		{RelativePath: "Artist/Song3.flac"},
	}

	// Song1 is under /music, Song2 is under /archive.
	// Both should be found and removed.
	targets := map[string]struct{}{
		"/music/Artist/Song1.flac": {},
		"/archive/Band/Song2.flac": {},
	}

	result := removeM3UEntries(
		entries, targets, []string{"/music", "/archive"},
	)

	if len(result) != 1 {
		t.Fatalf("expected 1 entry, got %d", len(result))
	}

	if result[0].RelativePath != "Artist/Song3.flac" {
		t.Errorf(
			"entry[0] = %q, want %q",
			result[0].RelativePath,
			"Artist/Song3.flac",
		)
	}
}

func TestFindM3UEntryMultiRoot(t *testing.T) {
	t.Parallel()

	entries := []m3uEntry{
		{RelativePath: "Artist/Song1.flac"},
		{RelativePath: "Band/Song2.flac"},
	}

	// Song2 is under the second root /archive.
	entry, idx := findM3UEntry(
		entries, "/archive/Band/Song2.flac",
		[]string{"/music", "/archive"},
	)

	if idx != 1 {
		t.Errorf("expected index 1, got %d", idx)
	}

	if entry.RelativePath != "Band/Song2.flac" {
		t.Errorf(
			"entry.RelativePath = %q, want %q",
			entry.RelativePath,
			"Band/Song2.flac",
		)
	}

	// Not found in any root.
	_, idx = findM3UEntry(
		entries, "/other/Missing.flac",
		[]string{"/music", "/archive"},
	)

	if idx != -1 {
		t.Errorf("expected index -1, got %d", idx)
	}
}

func TestReplaceM3UEntryPathsMultiRoot(t *testing.T) {
	t.Parallel()

	entries := []m3uEntry{
		{
			RelativePath: "old/path/song.flac",
			DurationSec:  180,
			DisplayTitle: "Song",
		},
		{
			RelativePath: "other/track.mp3",
			DurationSec:  240,
			DisplayTitle: "Track",
		},
	}

	// The old path resolves under /archive, not /music.
	replacements := map[string]string{
		"/archive/old/path/song.flac": "new/path/song.flac",
	}

	result := replaceM3UEntryPaths(
		entries, replacements,
		[]string{"/music", "/archive"},
	)

	if len(result) != 2 {
		t.Fatalf("expected 2 entries, got %d", len(result))
	}

	if result[0].RelativePath != "new/path/song.flac" {
		t.Errorf(
			"entry[0].RelativePath = %q, want %q",
			result[0].RelativePath,
			"new/path/song.flac",
		)
	}

	if result[0].DurationSec != 180 {
		t.Errorf(
			"entry[0].DurationSec = %d, want 180",
			result[0].DurationSec,
		)
	}

	if result[1].RelativePath != "other/track.mp3" {
		t.Errorf(
			"entry[1].RelativePath = %q, want %q",
			result[1].RelativePath,
			"other/track.mp3",
		)
	}
}
