package cmd

import (
	"bytes"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// captureStdout runs fn with os.Stdout redirected to a buffer and returns what
// was written.
func captureStdout(t *testing.T, fn func() error) (string, error) {
	t.Helper()

	r, w, err := os.Pipe()
	if err != nil {
		t.Fatalf("os.Pipe: %v", err)
	}
	old := os.Stdout
	os.Stdout = w

	done := make(chan struct{})
	var buf bytes.Buffer
	go func() {
		_, _ = io.Copy(&buf, r)
		close(done)
	}()

	runErr := fn()
	w.Close()
	<-done
	os.Stdout = old
	return buf.String(), runErr
}

func writeAudioFile(t *testing.T, path string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatalf("MkdirAll: %v", err)
	}
	if err := os.WriteFile(path, []byte{}, 0o644); err != nil {
		t.Fatalf("WriteFile: %v", err)
	}
}

func setupTestEnv(t *testing.T) string {
	t.Helper()
	home := t.TempDir()
	t.Setenv("HOME", home)
	return home
}

func TestPlaylistListEmpty(t *testing.T) {
	setupTestEnv(t)

	out, err := captureStdout(t, PlaylistList)
	if err != nil {
		t.Fatalf("PlaylistList: %v", err)
	}
	if !strings.Contains(out, "No playlists") {
		t.Errorf("output = %q, want 'No playlists...'", out)
	}
}

func TestPlaylistCreateAndList(t *testing.T) {
	home := setupTestEnv(t)
	audioDir := filepath.Join(home, "music")
	writeAudioFile(t, filepath.Join(audioDir, "song1.mp3"))
	writeAudioFile(t, filepath.Join(audioDir, "song2.flac"))

	// Create
	out, err := captureStdout(t, func() error {
		return PlaylistCreate("mymix", []string{audioDir}, "")
	})
	if err != nil {
		t.Fatalf("PlaylistCreate: %v", err)
	}
	if !strings.Contains(out, "Created playlist") {
		t.Errorf("create output = %q, want 'Created playlist'", out)
	}

	// List
	out, err = captureStdout(t, PlaylistList)
	if err != nil {
		t.Fatalf("PlaylistList: %v", err)
	}
	if !strings.Contains(out, "mymix") {
		t.Errorf("list output = %q, want to mention 'mymix'", out)
	}
	if !strings.Contains(out, "2 tracks") {
		t.Errorf("list output = %q, want '2 tracks'", out)
	}
}

func TestPlaylistCreateNoAudio(t *testing.T) {
	home := setupTestEnv(t)
	emptyDir := filepath.Join(home, "empty")
	if err := os.MkdirAll(emptyDir, 0o755); err != nil {
		t.Fatalf("MkdirAll: %v", err)
	}

	err := PlaylistCreate("nothing", []string{emptyDir}, "")
	if err == nil {
		t.Fatal("PlaylistCreate with no audio should error")
	}
	if !strings.Contains(err.Error(), "no audio") {
		t.Errorf("error = %q, want to mention 'no audio'", err.Error())
	}
}

func TestPlaylistCreateDuplicate(t *testing.T) {
	home := setupTestEnv(t)
	audio := filepath.Join(home, "a.mp3")
	writeAudioFile(t, audio)

	if err := PlaylistCreate("dup", []string{audio}, ""); err != nil {
		t.Fatalf("first PlaylistCreate: %v", err)
	}
	err := PlaylistCreate("dup", []string{audio}, "")
	if err == nil {
		t.Error("duplicate create should error")
	}
	if !strings.Contains(err.Error(), "already exists") {
		t.Errorf("error = %q, want to mention 'already exists'", err.Error())
	}
}

func TestPlaylistAddAppends(t *testing.T) {
	home := setupTestEnv(t)
	a := filepath.Join(home, "a.mp3")
	b := filepath.Join(home, "b.mp3")
	writeAudioFile(t, a)
	writeAudioFile(t, b)

	if err := PlaylistCreate("mix", []string{a}, ""); err != nil {
		t.Fatalf("PlaylistCreate: %v", err)
	}

	if err := PlaylistAdd("mix", []string{b}); err != nil {
		t.Fatalf("PlaylistAdd: %v", err)
	}

	out, _ := captureStdout(t, func() error { return PlaylistShow("mix", false) })
	if !strings.Contains(out, "2 tracks") {
		t.Errorf("Show output = %q, want '2 tracks' after add", out)
	}
}

func TestPlaylistAddNonExistent(t *testing.T) {
	home := setupTestEnv(t)
	a := filepath.Join(home, "a.mp3")
	writeAudioFile(t, a)

	err := PlaylistAdd("ghost", []string{a})
	if err == nil {
		t.Error("PlaylistAdd on non-existent playlist should error")
	}
}

func TestPlaylistShowEmpty(t *testing.T) {
	setupTestEnv(t)
	err := PlaylistShow("ghost", false)
	if err == nil {
		t.Error("PlaylistShow of missing playlist should error")
	}
}

func TestPlaylistShowJSON(t *testing.T) {
	home := setupTestEnv(t)
	audio := filepath.Join(home, "a.mp3")
	writeAudioFile(t, audio)
	if err := PlaylistCreate("mix", []string{audio}, ""); err != nil {
		t.Fatalf("PlaylistCreate: %v", err)
	}

	out, err := captureStdout(t, func() error { return PlaylistShow("mix", true) })
	if err != nil {
		t.Fatalf("PlaylistShow JSON: %v", err)
	}
	if !strings.HasPrefix(strings.TrimSpace(out), "[") {
		t.Errorf("JSON output should start with '[': %s", out)
	}
	if !strings.Contains(out, "\"path\"") {
		t.Errorf("JSON output should contain 'path' key: %s", out)
	}
}

func TestPlaylistRemove(t *testing.T) {
	home := setupTestEnv(t)
	a := filepath.Join(home, "a.mp3")
	b := filepath.Join(home, "b.mp3")
	writeAudioFile(t, a)
	writeAudioFile(t, b)
	if err := PlaylistCreate("mix", []string{a, b}, ""); err != nil {
		t.Fatalf("PlaylistCreate: %v", err)
	}

	if err := PlaylistRemove("mix", 1); err != nil {
		t.Fatalf("PlaylistRemove: %v", err)
	}

	out, _ := captureStdout(t, func() error { return PlaylistShow("mix", false) })
	if !strings.Contains(out, "1 tracks") {
		t.Errorf("output = %q, want '1 tracks' after remove", out)
	}
}

func TestPlaylistRemoveOutOfRange(t *testing.T) {
	home := setupTestEnv(t)
	a := filepath.Join(home, "a.mp3")
	writeAudioFile(t, a)
	if err := PlaylistCreate("mix", []string{a}, ""); err != nil {
		t.Fatalf("PlaylistCreate: %v", err)
	}

	err := PlaylistRemove("mix", 999)
	if err == nil {
		t.Error("PlaylistRemove with out-of-range index should error")
	}
}

func TestPlaylistDelete(t *testing.T) {
	home := setupTestEnv(t)
	audio := filepath.Join(home, "a.mp3")
	writeAudioFile(t, audio)
	if err := PlaylistCreate("todelete", []string{audio}, ""); err != nil {
		t.Fatalf("PlaylistCreate: %v", err)
	}

	if err := PlaylistDelete("todelete"); err != nil {
		t.Fatalf("PlaylistDelete: %v", err)
	}

	out, _ := captureStdout(t, PlaylistList)
	if strings.Contains(out, "todelete") {
		t.Errorf("after delete, List output still contains 'todelete': %s", out)
	}
}

func TestPlaylistDeleteMissing(t *testing.T) {
	setupTestEnv(t)
	err := PlaylistDelete("ghost")
	if err == nil {
		t.Error("PlaylistDelete on missing playlist should error")
	}
}

func TestPlaylistBookmarkToggle(t *testing.T) {
	home := setupTestEnv(t)
	audio := filepath.Join(home, "a.mp3")
	writeAudioFile(t, audio)
	if err := PlaylistCreate("mix", []string{audio}, ""); err != nil {
		t.Fatalf("PlaylistCreate: %v", err)
	}

	// Bookmark on.
	out, err := captureStdout(t, func() error { return PlaylistBookmark("mix", 1) })
	if err != nil {
		t.Fatalf("PlaylistBookmark: %v", err)
	}
	if !strings.Contains(out, "★") {
		t.Errorf("first bookmark output = %q, want '★'", out)
	}

	// Bookmark off.
	out, err = captureStdout(t, func() error { return PlaylistBookmark("mix", 1) })
	if err != nil {
		t.Fatalf("PlaylistBookmark toggle: %v", err)
	}
	if !strings.Contains(out, "☆") {
		t.Errorf("second bookmark output = %q, want '☆'", out)
	}
}

func TestPlaylistBookmarksEmpty(t *testing.T) {
	setupTestEnv(t)
	out, err := captureStdout(t, PlaylistBookmarks)
	if err != nil {
		t.Fatalf("PlaylistBookmarks: %v", err)
	}
	if !strings.Contains(out, "No bookmarks") {
		t.Errorf("output = %q, want 'No bookmarks...'", out)
	}
}

func TestPlaylistBookmarksShowsStars(t *testing.T) {
	home := setupTestEnv(t)
	audio := filepath.Join(home, "a.mp3")
	writeAudioFile(t, audio)
	if err := PlaylistCreate("mix", []string{audio}, ""); err != nil {
		t.Fatalf("PlaylistCreate: %v", err)
	}
	// Bookmark (captureStdout silences the toggle output).
	_, _ = captureStdout(t, func() error { return PlaylistBookmark("mix", 1) })

	out, err := captureStdout(t, PlaylistBookmarks)
	if err != nil {
		t.Fatalf("PlaylistBookmarks: %v", err)
	}
	if !strings.Contains(out, "★") {
		t.Errorf("bookmarks output = %q, want '★'", out)
	}
}

func TestCollectLocalAudioMultiplePaths(t *testing.T) {
	dir := t.TempDir()
	a := filepath.Join(dir, "a.mp3")
	b := filepath.Join(dir, "sub", "b.flac")
	writeAudioFile(t, a)
	writeAudioFile(t, b)

	got, err := collectLocalAudio([]string{a, b})
	if err != nil {
		t.Fatalf("collectLocalAudio: %v", err)
	}
	if len(got) != 2 {
		t.Errorf("got %d paths, want 2 — %v", len(got), got)
	}
}

func TestCollectLocalAudioMissingPath(t *testing.T) {
	_, err := collectLocalAudio([]string{filepath.Join(t.TempDir(), "nope")})
	if err == nil {
		t.Error("collectLocalAudio with missing path should error")
	}
}

func TestNewProvider(t *testing.T) {
	setupTestEnv(t)
	p, err := newProvider()
	if err != nil {
		t.Fatalf("newProvider: %v", err)
	}
	if p == nil {
		t.Error("newProvider returned nil provider")
	}
}
