package playlist

import (
	"testing"
)

// helper builds a playlist with n tracks named "A", "B", "C", ...
func makePlaylist(n int, shuffle bool) *Playlist {
	tracks := make([]Track, n)
	for i := range tracks {
		tracks[i] = Track{Title: string(rune('A' + i))}
	}
	p := New()
	if shuffle {
		p.shuffle = true
	}
	p.Replace(tracks)
	return p
}

func titles(p *Playlist) []string {
	out := make([]string, p.Len())
	for i, t := range p.Tracks() {
		out[i] = t.Title
	}
	return out
}

func sliceEq(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

func TestMoveDown(t *testing.T) {
	p := makePlaylist(5, false) // A B C D E
	p.SetIndex(0)               // playing A

	if !p.Move(1, 2) {
		t.Fatal("Move returned false")
	}

	// Visual order: A C B D E
	got := titles(p)
	want := []string{"A", "C", "B", "D", "E"}
	if !sliceEq(got, want) {
		t.Errorf("tracks = %v, want %v", got, want)
	}

	// Still playing A
	if _, idx := p.Current(); idx != 0 {
		t.Errorf("current index = %d, want 0", idx)
	}
}

func TestMoveUp(t *testing.T) {
	p := makePlaylist(5, false) // A B C D E
	p.SetIndex(3)               // playing D

	if !p.Move(3, 2) {
		t.Fatal("Move returned false")
	}

	// Visual order: A B D C E
	got := titles(p)
	want := []string{"A", "B", "D", "C", "E"}
	if !sliceEq(got, want) {
		t.Errorf("tracks = %v, want %v", got, want)
	}

	// Still playing D, now at index 2
	if _, idx := p.Current(); idx != 2 {
		t.Errorf("current index = %d, want 2", idx)
	}
}

func TestMoveCurrentTrack(t *testing.T) {
	p := makePlaylist(4, false) // A B C D
	p.SetIndex(1)               // playing B

	// Move B down (from 1 to 2)
	if !p.Move(1, 2) {
		t.Fatal("Move returned false")
	}

	// Visual: A C B D
	got := titles(p)
	want := []string{"A", "C", "B", "D"}
	if !sliceEq(got, want) {
		t.Errorf("tracks = %v, want %v", got, want)
	}

	// Still playing B, now at index 2
	if _, idx := p.Current(); idx != 2 {
		t.Errorf("current index = %d, want 2", idx)
	}
}

func TestMoveBoundary(t *testing.T) {
	p := makePlaylist(3, false)

	// Can't move first track up
	if p.Move(0, -1) {
		t.Error("Move(0, -1) should return false")
	}

	// Can't move last track down
	if p.Move(2, 3) {
		t.Error("Move(2, 3) should return false")
	}

	// Same position is a no-op
	if p.Move(1, 1) {
		t.Error("Move(1, 1) should return false")
	}
}

func TestMovePreservesPlaybackOrder_NoShuffle(t *testing.T) {
	p := makePlaylist(5, false) // A B C D E
	p.SetIndex(0)

	// Move C (2) up to (1): A C B D E
	p.Move(2, 1)

	// Playback should follow new visual order: A C B D E
	var playback []string
	track, _ := p.Current()
	playback = append(playback, track.Title) // A

	for i := range 4 {
		track, ok := p.Next()
		if !ok {
			t.Fatalf("Next() returned false at step %d", i)
		}
		playback = append(playback, track.Title)
	}

	want := []string{"A", "C", "B", "D", "E"}
	if !sliceEq(playback, want) {
		t.Errorf("playback = %v, want %v", playback, want)
	}
}

func TestMoveWithQueue(t *testing.T) {
	p := makePlaylist(4, false) // A B C D
	p.Queue(2)                  // queue C (index 2)

	// Move C (2) up to (1): A C B D, queue should now reference index 1
	p.Move(2, 1)

	if pos := p.QueuePosition(1); pos != 1 {
		t.Errorf("QueuePosition(1) = %d, want 1", pos)
	}
	if pos := p.QueuePosition(2); pos != 0 {
		t.Errorf("QueuePosition(2) = %d, want 0 (not queued)", pos)
	}
}

func TestMoveShuffle(t *testing.T) {
	p := makePlaylist(5, true) // shuffled

	// Record the current shuffle playback order
	p.SetIndex(p.order[0])
	// Snapshot the order
	orderBefore := make([]int, len(p.order))
	copy(orderBefore, p.order)

	// Move tracks[1] to tracks[0]
	t0 := p.tracks[0].Title
	t1 := p.tracks[1].Title
	p.Move(1, 0)

	// Visual order should be swapped at 0,1
	if p.tracks[0].Title != t1 || p.tracks[1].Title != t0 {
		t.Errorf("tracks[0]=%s tracks[1]=%s, want %s %s",
			p.tracks[0].Title, p.tracks[1].Title, t1, t0)
	}

	// The shuffle order should still reference the same tracks
	for i, idx := range p.order {
		got := p.tracks[idx].Title
		want := ""
		oldIdx := orderBefore[i]
		// The old index pointed to a track; after swap, find where it went
		if oldIdx == 0 {
			want = t0
		} else if oldIdx == 1 {
			want = t1
		} else {
			want = string(rune('A' + oldIdx))
		}
		if got != want {
			t.Errorf("order[%d]: track=%s, want=%s", i, got, want)
		}
	}
}

func TestAddShufflesNewTracksWhenShuffleEnabled(t *testing.T) {
	p := makePlaylist(10, true)
	p.SetIndex(p.order[0]) // ensure pos is valid and stable
	cur, curIdx := p.Current()

	start := p.Len()
	var added []Track
	for i := range 30 {
		added = append(added, Track{Title: string(rune('K' + i))})
	}
	p.Add(added...)

	// Current track should be unchanged.
	cur2, curIdx2 := p.Current()
	if cur2.Title != cur.Title || curIdx2 != curIdx {
		t.Fatalf("current = (%q,%d), want (%q,%d)", cur2.Title, curIdx2, cur.Title, curIdx)
	}

	// Verify that added tracks are interleaved with existing upcoming tracks,
	// not just shuffled among themselves at the tail.
	upcoming := p.order[p.pos+1:]
	isNew := func(idx int) bool { return idx >= start }
	// Find the last new-track position and check that at least one
	// old track appears after some new track in the upcoming order.
	lastNew := -1
	foundOldAfterNew := false
	for i, idx := range upcoming {
		if isNew(idx) {
			lastNew = i
		} else if lastNew >= 0 {
			foundOldAfterNew = true
			break
		}
	}
	if lastNew < 0 {
		t.Fatal("no added track found in upcoming order")
	}
	if !foundOldAfterNew && lastNew < len(upcoming)-1 {
		t.Fatalf("added tracks are not interleaved with existing tracks in upcoming order: %v", upcoming)
	}
}

func TestAddDoesNotShuffleWhenShuffleDisabled(t *testing.T) {
	p := makePlaylist(5, false)
	p.SetIndex(2)
	cur, curIdx := p.Current()

	p.Add(Track{Title: "F"}, Track{Title: "G"})

	cur2, curIdx2 := p.Current()
	if cur2.Title != cur.Title || curIdx2 != curIdx {
		t.Fatalf("current = (%q,%d), want (%q,%d)", cur2.Title, curIdx2, cur.Title, curIdx)
	}

	wantOrder := []int{0, 1, 2, 3, 4, 5, 6}
	if len(p.order) != len(wantOrder) {
		t.Fatalf("order len = %d, want %d", len(p.order), len(wantOrder))
	}
	for i := range wantOrder {
		if p.order[i] != wantOrder[i] {
			t.Fatalf("order[%d] = %d, want %d (order=%v)", i, p.order[i], wantOrder[i], p.order)
		}
	}
}

func TestMoveQueue(t *testing.T) {
	p := makePlaylist(5, false) // A B C D E
	p.Queue(3)                  // D
	p.Queue(1)                  // B
	p.Queue(4)                  // E
	// Queue order: D, B, E

	// Move B (pos 1) up to pos 0
	if !p.MoveQueue(1, 0) {
		t.Fatal("MoveQueue returned false")
	}
	// Queue order should be: B, D, E
	qt := p.QueueTracks()
	if len(qt) != 3 {
		t.Fatalf("queue len = %d, want 3", len(qt))
	}
	if qt[0].Title != "B" || qt[1].Title != "D" || qt[2].Title != "E" {
		t.Errorf("queue = [%s %s %s], want [B D E]", qt[0].Title, qt[1].Title, qt[2].Title)
	}

	// Move B (pos 0) down to pos 1
	if !p.MoveQueue(0, 1) {
		t.Fatal("MoveQueue returned false")
	}
	// Queue order should be: D, B, E
	qt = p.QueueTracks()
	if qt[0].Title != "D" || qt[1].Title != "B" || qt[2].Title != "E" {
		t.Errorf("queue = [%s %s %s], want [D B E]", qt[0].Title, qt[1].Title, qt[2].Title)
	}
}

func TestRemoveShiftsHigherIndices(t *testing.T) {
	p := makePlaylist(5, false) // A B C D E
	p.SetIndex(3)               // playing D
	p.Queue(4)                  // queue E

	if !p.Remove(1) { // remove B
		t.Fatal("Remove returned false")
	}

	got := titles(p)
	want := []string{"A", "C", "D", "E"}
	if !sliceEq(got, want) {
		t.Errorf("tracks = %v, want %v", got, want)
	}

	cur, idx := p.Current()
	if cur.Title != "D" || idx != 2 {
		t.Errorf("current = (%s, %d), want (D, 2)", cur.Title, idx)
	}

	qt := p.QueueTracks()
	if len(qt) != 1 || qt[0].Title != "E" {
		t.Errorf("queue = %v, want [E]", qt)
	}
}

func TestRemoveCurrentTrack(t *testing.T) {
	p := makePlaylist(4, false) // A B C D
	p.SetIndex(1)               // playing B

	if !p.Remove(1) {
		t.Fatal("Remove returned false")
	}

	got := titles(p)
	want := []string{"A", "C", "D"}
	if !sliceEq(got, want) {
		t.Errorf("tracks = %v, want %v", got, want)
	}

	// Position stays at order slot 1 so Next/PeekNext sees the next track.
	cur, idx := p.Current()
	if cur.Title != "C" || idx != 1 {
		t.Errorf("current = (%s, %d), want (C, 1)", cur.Title, idx)
	}
}

func TestRemoveLastTrack(t *testing.T) {
	p := makePlaylist(2, false) // A B
	p.SetIndex(1)               // playing B

	if !p.Remove(1) {
		t.Fatal("Remove returned false")
	}

	if p.Len() != 1 {
		t.Fatalf("len = %d, want 1", p.Len())
	}
	cur, idx := p.Current()
	if cur.Title != "A" || idx != 0 {
		t.Errorf("current = (%s, %d), want (A, 0)", cur.Title, idx)
	}
}

func TestRemoveOutOfBounds(t *testing.T) {
	p := makePlaylist(3, false)
	if p.Remove(-1) {
		t.Error("Remove(-1) should return false")
	}
	if p.Remove(3) {
		t.Error("Remove(3) should return false")
	}
	if p.Len() != 3 {
		t.Errorf("len changed to %d", p.Len())
	}
}

func TestMoveQueueBoundary(t *testing.T) {
	p := makePlaylist(3, false)
	p.Queue(0)
	p.Queue(1)

	// Can't move beyond bounds
	if p.MoveQueue(0, -1) {
		t.Error("MoveQueue(0, -1) should return false")
	}
	if p.MoveQueue(1, 2) {
		t.Error("MoveQueue(1, 2) should return false")
	}
	if p.MoveQueue(0, 0) {
		t.Error("MoveQueue(0, 0) should return false")
	}
}

func TestNextPreservesCurrentOnUnplayableTail(t *testing.T) {
	p := New()
	p.Replace([]Track{
		{Title: "A"},
		{Title: "B", Unplayable: true},
		{Title: "C", Unplayable: true},
	})
	p.SetIndex(0)

	if _, ok := p.Next(); ok {
		t.Fatal("Next() = true, want false")
	}

	track, idx := p.Current()
	if track.Title != "A" || idx != 0 {
		t.Fatalf("current = (%q,%d), want (\"A\",0)", track.Title, idx)
	}
}

func TestPrevPreservesCurrentOnUnplayableHead(t *testing.T) {
	p := New()
	p.Replace([]Track{
		{Title: "A", Unplayable: true},
		{Title: "B", Unplayable: true},
		{Title: "C"},
	})
	p.SetIndex(2)

	if _, ok := p.Prev(); ok {
		t.Fatal("Prev() = true, want false")
	}

	track, idx := p.Current()
	if track.Title != "C" || idx != 2 {
		t.Fatalf("current = (%q,%d), want (\"C\",2)", track.Title, idx)
	}
}

func TestPeekNextMatchesNext(t *testing.T) {
	p := New()
	p.Replace([]Track{
		{Title: "A"},
		{Title: "B", Unplayable: true},
		{Title: "C"},
	})
	p.SetIndex(0)
	p.Queue(1)
	p.Queue(2)

	peek, ok := p.PeekNext()
	if !ok {
		t.Fatal("PeekNext() = false, want true")
	}
	if peek.Title != "C" {
		t.Fatalf("peek = %q, want %q", peek.Title, "C")
	}
	cur, idx := p.Current()
	if cur.Title != "A" || idx != 0 {
		t.Fatalf("current after peek = (%q,%d), want (\"A\",0)", cur.Title, idx)
	}
	if p.QueueLen() != 2 {
		t.Fatalf("QueueLen() after peek = %d, want 2", p.QueueLen())
	}

	next, ok := p.Next()
	if !ok {
		t.Fatal("Next() = false, want true")
	}
	if next.Title != peek.Title {
		t.Fatalf("next = %q, want %q", next.Title, peek.Title)
	}
}

func TestNextConsumesUnplayableQueuedItemsOnFailure(t *testing.T) {
	p := New()
	p.Replace([]Track{
		{Title: "A"},
		{Title: "B", Unplayable: true},
	})
	p.SetIndex(0)
	p.Queue(1)

	if _, ok := p.Next(); ok {
		t.Fatal("Next() = true, want false")
	}
	cur, idx := p.Current()
	if cur.Title != "A" || idx != 0 {
		t.Fatalf("current = (%q,%d), want (\"A\",0)", cur.Title, idx)
	}
	if p.QueueLen() != 0 {
		t.Fatalf("QueueLen() = %d, want 0", p.QueueLen())
	}
}

func TestNextFailurePreservesQueuedCurrentTrack(t *testing.T) {
	p := New()
	p.Replace([]Track{
		{Title: "A"},
		{Title: "B"},
	})
	p.SetIndex(1)
	p.Queue(0)

	track, ok := p.Next()
	if !ok || track.Title != "A" {
		t.Fatalf("first Next() = (%q,%v), want (A,true)", track.Title, ok)
	}

	if _, ok := p.Next(); ok {
		t.Fatal("second Next() = true, want false")
	}
	cur, idx := p.Current()
	if cur.Title != "A" || idx != 0 {
		t.Fatalf("current after failed Next() = (%q,%d), want (A,0)", cur.Title, idx)
	}
}

func TestNextRepeatAllShuffleWrapSkipsCurrentTrack(t *testing.T) {
	p := New()
	p.shuffle = true
	p.repeat = RepeatAll
	p.Replace([]Track{
		{Title: "A"},
		{Title: "B"},
		{Title: "C", Unplayable: true},
	})
	p.order = []int{1, 2, 0}
	p.pos = 2

	track, ok := p.Next()
	if !ok {
		t.Fatal("Next() = false, want true")
	}
	if track.Title != "B" {
		t.Fatalf("next = %q, want %q", track.Title, "B")
	}
	if _, idx := p.Current(); idx != 1 {
		t.Fatalf("current index = %d, want 1", idx)
	}
}

func TestNextRepeatAllShuffleWrapFailureKeepsCurrentTrack(t *testing.T) {
	p := New()
	p.shuffle = true
	p.repeat = RepeatAll
	p.Replace([]Track{
		{Title: "A", Unplayable: true},
		{Title: "B", Unplayable: true},
		{Title: "C", Unplayable: true},
	})
	p.pos = len(p.order) - 1

	before, beforeIdx := p.Current()

	if _, ok := p.Next(); ok {
		t.Fatal("Next() = true, want false")
	}

	after, afterIdx := p.Current()
	if after.Title != before.Title || afterIdx != beforeIdx {
		t.Fatalf("current = (%q,%d), want (%q,%d)", after.Title, afterIdx, before.Title, beforeIdx)
	}
}

func TestNextRepeatOneUnplayableCurrentReturnsFalse(t *testing.T) {
	p := New()
	p.repeat = RepeatOne
	p.Replace([]Track{
		{Title: "A", Unplayable: true},
		{Title: "B"},
		{Title: "C"},
	})
	p.SetIndex(0)

	if _, ok := p.Next(); ok {
		t.Fatal("Next() = true, want false")
	}
	if track, idx := p.Current(); track.Title != "A" || idx != 0 {
		t.Fatalf("current = (%q,%d), want (\"A\",0)", track.Title, idx)
	}
}

func TestActivateSelectedUsesSelectedOrderEvenWhenQueueChangesCurrent(t *testing.T) {
	tests := []struct {
		name           string
		prepare        func(t *testing.T, p *Playlist)
		wantQueueLen   int
		wantQueueTrack int
	}{
		{
			name: "pending queued track",
			prepare: func(_ *testing.T, p *Playlist) {
				p.Queue(0)
			},
			wantQueueLen:   1,
			wantQueueTrack: 0,
		},
		{
			name: "active queued current",
			prepare: func(t *testing.T, p *Playlist) {
				p.Queue(0)
				if track, ok := p.Next(); !ok || track.Title != "Queued" {
					t.Fatalf("Next() = (%q,%t), want (\"Queued\",true)", track.Title, ok)
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := New()
			p.Replace([]Track{
				{Title: "Queued"},
				{Title: "Missing", Unplayable: true},
				{Title: "Replacement"},
			})
			p.SetIndex(1)
			tt.prepare(t, p)

			activation, ok := p.ActivateSelected()
			if !ok {
				t.Fatal("ActivateSelected() = false, want true")
			}
			if activation.Track.Title != "Replacement" || activation.Index != 2 || !activation.Skipped {
				t.Fatalf("activation = (%q,%d,%t), want (\"Replacement\",2,true)", activation.Track.Title, activation.Index, activation.Skipped)
			}
			if current, idx := p.Current(); current.Title != "Replacement" || idx != 2 {
				t.Fatalf("current = (%q,%d), want (\"Replacement\",2)", current.Title, idx)
			}
			if p.QueueLen() != tt.wantQueueLen {
				t.Fatalf("QueueLen() = %d, want %d", p.QueueLen(), tt.wantQueueLen)
			}
			if tt.wantQueueLen > 0 && p.QueuePosition(tt.wantQueueTrack) != 1 {
				t.Fatalf("QueuePosition(%d) = %d, want 1", tt.wantQueueTrack, p.QueuePosition(tt.wantQueueTrack))
			}
		})
	}
}

func TestActivateSelectedWrapsWithRepeatAll(t *testing.T) {
	p := New()
	p.repeat = RepeatAll
	p.Replace([]Track{
		{Title: "A"},
		{Title: "B"},
		{Title: "C", Unplayable: true},
	})
	p.SetIndex(2)

	activation, ok := p.ActivateSelected()
	if !ok {
		t.Fatal("ActivateSelected() = false, want true")
	}
	if activation.Track.Title != "A" || activation.Index != 0 || !activation.Skipped {
		t.Fatalf("activation = (%q,%d,%t), want (\"A\",0,true)", activation.Track.Title, activation.Index, activation.Skipped)
	}
}

func TestActivateSelectedFailureKeepsQueuedCurrentTrack(t *testing.T) {
	p := New()
	p.Replace([]Track{
		{Title: "Queued"},
		{Title: "Missing", Unplayable: true},
		{Title: "Still Missing", Unplayable: true},
	})
	p.SetIndex(1)
	p.Queue(0)
	p.Queue(2)
	if track, ok := p.Next(); !ok || track.Title != "Queued" {
		t.Fatalf("Next() = (%q,%t), want (\"Queued\",true)", track.Title, ok)
	}

	if _, ok := p.ActivateSelected(); ok {
		t.Fatal("ActivateSelected() = true, want false")
	}
	if current, idx := p.Current(); current.Title != "Queued" || idx != 0 {
		t.Fatalf("current = (%q,%d), want (\"Queued\",0)", current.Title, idx)
	}
	if p.QueueLen() != 1 {
		t.Fatalf("QueueLen() = %d, want 1", p.QueueLen())
	}
	if p.QueuePosition(2) != 1 {
		t.Fatalf("QueuePosition(2) = %d, want 1", p.QueuePosition(2))
	}
}

func TestTotalDurationSecs(t *testing.T) {
	tracks := []Track{
		{DurationSecs: 100},
		{DurationSecs: 0}, // unknown — skipped
		{DurationSecs: 200},
	}
	if got := TotalDurationSecs(tracks); got != 300 {
		t.Errorf("TotalDurationSecs = %d, want 300", got)
	}
	if got := TotalDurationSecs(nil); got != 0 {
		t.Errorf("TotalDurationSecs(nil) = %d, want 0", got)
	}
}
