package ui

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/muesli/termenv"

	"github.com/gmgauthier/gostations/internal/radio"
)

func TestApp_BasicKeyHandling(t *testing.T) {
	app := NewApp([]radio.Station{
		{Name: "Test1", Url: "http://a", Codec: "MP3", Bitrate: "128"},
	})

	// Send q
	model, cmd := app.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}})
	if !model.(*App).quitting {
		t.Error("q did not set quitting")
	}
	_ = cmd

	// Send window size
	model, _ = app.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
	a := model.(*App)
	if a.width != 80 {
		t.Errorf("expected app.width=80 after WindowSizeMsg, got %d", a.width)
	}
	if a.list.Width() == 0 {
		t.Log("list size not updated (may be ok in test)")
	}
}

func TestApp_AutoFilterOnTyping(t *testing.T) {
	app := NewApp([]radio.Station{
		{Name: "WFMT 98.7", Url: "http://wfmt", Codec: "MP3", Bitrate: "128", Tags: "chicago,classical"},
		{Name: "Other Station", Url: "http://other", Codec: "AAC", Bitrate: "64", Tags: "news"},
	})

	// Simulate typing 'W' (auto enter filter). Drive any returned cmds.
	model, cmd := app.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'W'}})
	a := model.(*App)
	for cmd != nil {
		msg := cmd()
		if msg == nil {
			break
		}
		model, cmd = a.Update(msg.(tea.Msg))
		a = model.(*App)
	}

	fv := a.list.FilterValue()
	if fv != "W" {
		t.Errorf("expected filter value 'W' after typing W, got %q", fv)
	}
	visible := a.list.VisibleItems()
	if len(visible) == 0 {
		t.Error("expected some visible items after filter 'W'")
	}

	// Now type the rest of "WFMT", driving cmds each time
	for _, r := range "FMT" {
		model, cmd = a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}})
		a = model.(*App)
		for cmd != nil {
			msg := cmd()
			if msg == nil {
				break
			}
			model, cmd = a.Update(msg.(tea.Msg))
			a = model.(*App)
		}
	}

	fv = a.list.FilterValue()
	if fv != "WFMT" {
		t.Errorf("expected filter value 'WFMT', got %q", fv)
	}

	// Note: in live typing simulation, state may stay 'filtering' and filteredItems update may depend on internal cmd processing.
	// For verifying the custom substring filter logic itself, use SetFilterText which synchronously applies.
	a.list.SetFilterText("WFMT")

	visible = a.list.VisibleItems()
	if len(visible) != 1 {
		t.Errorf("expected exactly 1 item for 'WFMT' substring filter, got %d", len(visible))
	}
	if len(visible) > 0 {
		if it, ok := visible[0].(item); ok {
			if it.station.Name != "WFMT 98.7" {
				t.Errorf("expected 'WFMT 98.7' to be the match, got %q", it.station.Name)
			}
		}
	}
}

func TestNewAppTitleForFavorites(t *testing.T) {
	// Create a temp XDG so NewApp's internal data.NewFavorites() will see our favs
	tmpDir := t.TempDir()
	os.Setenv("XDG_CONFIG_HOME", tmpDir)
	defer os.Unsetenv("XDG_CONFIG_HOME")

	// Write a real favorites.json so favSet inside NewApp will contain them
	favDir := filepath.Join(tmpDir, "gostations")
	if err := os.MkdirAll(favDir, 0755); err != nil {
		t.Fatal(err)
	}
	favData := []radio.Station{
		{Name: "Fav1", Url: "http://fav1", Codec: "MP3", Bitrate: "128"},
		{Name: "Fav2", Url: "http://fav2", Codec: "AAC", Bitrate: "64"},
	}
	b, _ := json.MarshalIndent(favData, "", "  ")
	if err := os.WriteFile(filepath.Join(favDir, "favorites.json"), b, 0644); err != nil {
		t.Fatal(err)
	}

	favStations := []radio.Station{
		{Name: "Fav1", Url: "http://fav1", Codec: "MP3", Bitrate: "128"},
		{Name: "Fav2", Url: "http://fav2", Codec: "AAC", Bitrate: "64"},
	}

	// Now NewApp will load the favs we just wrote, so heuristic will see them
	app := NewApp(favStations)
	if app.list.Title != "GoStations - Your Favorites" {
		t.Errorf("expected 'Your Favorites' title when initial == all favs, got %q", app.list.Title)
	}

	// Non-fav: generic
	nonFav := []radio.Station{{Name: "Random", Url: "http://rand", Codec: "MP3", Bitrate: "128"}}
	app2 := NewApp(nonFav)
	if app2.list.Title != "GoStations - Radio Browser (new TUI • ★ = favorite)" {
		t.Errorf("expected generic title, got %q", app2.list.Title)
	}

	app3 := NewApp(nil)
	if app3.list.Title != "GoStations - Radio Browser (new TUI • ★ = favorite)" {
		t.Errorf("expected generic for empty, got %q", app3.list.Title)
	}
}

func TestRenderHint_Visual(t *testing.T) {
	// Force color output so lipgloss always emits the bg/fg ANSI codes for the key badges
	// (in real TUI this happens automatically on a pty).
	prevProfile := lipgloss.ColorProfile()
	lipgloss.SetColorProfile(termenv.TrueColor)
	defer lipgloss.SetColorProfile(prevProfile)

	app := NewApp(nil)
	app.width = 120 // simulate a typical terminal width to exercise full-width bar
	h := app.renderHint()

	// Basic sanity: all the documented trigger keys are present in output.
	for _, want := range []string{"[/]", "[ENTER]", "[Q]", "[f]"} {
		if !strings.Contains(h, want) {
			t.Errorf("renderHint missing %s in %q", want, h)
		}
	}
	if !strings.Contains(h, "\x1b[") {
		t.Errorf("expected ANSI color codes from hintKeyStyle (bg+fg), got plain: %q", h)
	}
	// Show what a user will see (the escapes will be interpreted by the terminal in real run).
	visible := strings.ReplaceAll(h, "\x1b", "\\x1b")
	t.Logf("HINT ROW RENDERED (with forced color profile, width=120): %s", visible)

	// Rough check that the bar filled to (near) requested width (after ANSI codes).
	// We strip the known key/badge escapes for a simple length heuristic on the plain text + pads.
	plainish := strings.ReplaceAll(h, "\x1b[1;97;48;5;63m", "")
	plainish = strings.ReplaceAll(plainish, "\x1b[0m", "")
	plainish = strings.ReplaceAll(plainish, "\x1b[38;5;245m", "")
	if len(plainish) < 100 {
		t.Errorf("expected bar to be nearly full width (len after basic strip ~120), got %d: %q", len(plainish), plainish)
	}
}

// stubPlayer is a no-op player for unit tests (avoids real mpv exec + socket in tests).
type stubPlayer struct{}

func (stubPlayer) Play(url string, extra ...string) error { return nil }
func (stubPlayer) Stop() error                          { return nil }
func (stubPlayer) Metadata() string                     { return "Fake Song Title [stream]" }
func (stubPlayer) Pause() error                         { return nil }
func (stubPlayer) Resume() error                        { return nil }
func (stubPlayer) Mute() error                          { return nil }
func (stubPlayer) Unmute() error                        { return nil }
func (stubPlayer) Next() error                          { return nil }
func (stubPlayer) Prev() error                          { return nil }
func (stubPlayer) VolumeUp() error                      { return nil }
func (stubPlayer) VolumeDown() error                    { return nil }
func (stubPlayer) Volume() int                          { return 65 }

func TestApp_PlaybackView(t *testing.T) {
	stations := []radio.Station{
		{Name: "Test Radio", Url: "http://example.com/stream", Codec: "MP3", Bitrate: "128"},
	}
	app := NewApp(stations)
	// override with stub so no real process in test
	app.player = stubPlayer{}

	// size so render works
	app.Update(tea.WindowSizeMsg{Width: 80, Height: 24})

	// press enter on first (only) item
	app.Update(tea.KeyMsg{Type: tea.KeyEnter})

	if !app.playing {
		t.Fatal("expected to be in playing state after enter")
	}
	if app.playingItem.station.Name != "Test Radio" {
		t.Errorf("wrong station: %s", app.playingItem.station.Name)
	}
	if app.nowPlaying == "" {
		t.Error("nowPlaying should be initialized")
	}

	// poll would have set it
	app.Update(metadataMsg{title: "Fake Song Title [stream]"})
	if !strings.Contains(app.nowPlaying, "Fake") {
		t.Errorf("metadata not applied: %s", app.nowPlaying)
	}

	// exercise volume keys (no-op on stub, but covers the handler)
	app.Update(tea.KeyMsg{Type: tea.KeyUp})
	app.Update(tea.KeyMsg{Type: tea.KeyDown})

	// render while still playing (with metadata)
	v := app.renderPlayback()
	if !strings.Contains(v, "Test Radio") || !strings.Contains(v, "NOW PLAYING") {
		t.Errorf("playback render missing expected content: %s", v)
	}
	if !strings.Contains(v, "Fake Song") {
		t.Logf("note: metadata may not be in this render snapshot")
	}

	// check playing-mode hint bar includes volume
	app.playing = true
	h := app.renderHint()
	if !strings.Contains(h, "Vol") || !strings.Contains(h, "↑↓") {
		t.Errorf("playing hint missing volume info: %s", h)
	}

	// press s to stop
	app.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'s'}})
	if app.playing {
		t.Error("expected stopped after 's'")
	}
}

