package player

import (
	"math"
	"sync"
	"testing"
)

// newTestPlayer returns a Player with only the atomic accessors wired up.
// We deliberately skip speaker.Init (which would open a real audio device)
// by constructing the struct directly — this limits us to testing the
// lock-free getters/setters, not the audio pipeline itself.
func newTestPlayer() *Player {
	p := &Player{}
	p.volMin.Store(math.Float64bits(-50))
	p.speed.Store(math.Float64bits(1.0))
	return p
}

func TestSetVolumeClamps(t *testing.T) {
	p := newTestPlayer()

	tests := []struct {
		in   float64
		want float64
	}{
		{-60, -50}, // below min
		{-50, -50},
		{0, 0},
		{6, 6},
		{12, 6}, // above max
	}
	for _, tt := range tests {
		p.SetVolume(tt.in)
		if got := p.Volume(); got != tt.want {
			t.Errorf("SetVolume(%v) → Volume() = %v, want %v", tt.in, got, tt.want)
		}
	}
}

func TestSetVolumeMinClamps(t *testing.T) {
	p := newTestPlayer()

	tests := []struct {
		in   float64
		want float64
	}{
		{-100, -90}, // below absolute floor
		{-90, -90},
		{-50, -50},
		{0, 0},
		{5, 0}, // above max (must be ≤ 0)
	}
	for _, tt := range tests {
		p.SetVolumeMin(tt.in)
		if got := p.VolumeMin(); got != tt.want {
			t.Errorf("SetVolumeMin(%v) → VolumeMin() = %v, want %v", tt.in, got, tt.want)
		}
	}
}

func TestSetVolumeMinReconcilesCurrent(t *testing.T) {
	p := newTestPlayer()
	p.SetVolume(-40)
	p.SetVolumeMin(-30) // raise floor above current volume
	if got := p.Volume(); got != -30 {
		t.Errorf("Volume after SetVolumeMin raise = %v, want -30", got)
	}
}

func TestVolumeClampedToCustomMin(t *testing.T) {
	tests := []struct {
		name      string
		volumeMin float64
		volume    float64
		want      float64
	}{
		{"below floor", -30, -40, -30},
		{"at floor", -30, -30, -30},
		{"above floor", -30, -10, -10},
		{"custom floor -60", -60, -70, -60},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := newTestPlayer()
			p.SetVolumeMin(tt.volumeMin)
			p.SetVolume(tt.volume)
			if got := p.Volume(); got != tt.want {
				t.Errorf("Volume = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestSetSpeedClamps(t *testing.T) {
	p := newTestPlayer()

	tests := []struct {
		in   float64
		want float64
	}{
		{0.1, 0.25},
		{0.25, 0.25},
		{1.0, 1.0},
		{2.0, 2.0},
		{3.0, 2.0},
	}
	for _, tt := range tests {
		p.SetSpeed(tt.in)
		if got := p.Speed(); got != tt.want {
			t.Errorf("SetSpeed(%v) → Speed() = %v, want %v", tt.in, got, tt.want)
		}
	}
}

func TestToggleMono(t *testing.T) {
	p := newTestPlayer()

	if p.Mono() {
		t.Fatal("Mono() should start false")
	}
	p.ToggleMono()
	if !p.Mono() {
		t.Error("ToggleMono should flip to true")
	}
	p.ToggleMono()
	if p.Mono() {
		t.Error("ToggleMono should flip back to false")
	}
}

func TestSetEQBandClamps(t *testing.T) {
	p := newTestPlayer()

	tests := []struct {
		band int
		in   float64
		want float64
	}{
		{0, 20.0, 12.0},
		{0, -20.0, -12.0},
		{5, 6.5, 6.5},
		{9, 0.0, 0.0},
	}
	for _, tt := range tests {
		p.SetEQBand(tt.band, tt.in)
		bands := p.EQBands()
		if bands[tt.band] != tt.want {
			t.Errorf("SetEQBand(%d, %v) → %v, want %v", tt.band, tt.in, bands[tt.band], tt.want)
		}
	}
}

func TestSetEQBandIgnoresInvalidIndex(t *testing.T) {
	p := newTestPlayer()
	// Setting an out-of-range band should be a no-op, not panic.
	p.SetEQBand(-1, 5)
	p.SetEQBand(10, 5)
	p.SetEQBand(100, 5)

	for i, b := range p.EQBands() {
		if b != 0 {
			t.Errorf("EQBands[%d] = %v, want 0 after invalid writes", i, b)
		}
	}
}

func TestEQBandsReturnsCopy(t *testing.T) {
	p := newTestPlayer()
	p.SetEQBand(0, 6)

	bands := p.EQBands()
	bands[0] = 999 // mutate local copy

	if p.EQBands()[0] != 6 {
		t.Error("EQBands() should return a copy — mutation leaked back")
	}
}

func TestIsPlayingDefaultsFalse(t *testing.T) {
	p := newTestPlayer()
	if p.IsPlaying() {
		t.Error("IsPlaying() should start false")
	}
	if p.IsPaused() {
		t.Error("IsPaused() should start false")
	}
}

func TestSampleRate(t *testing.T) {
	p := &Player{sr: 44100}
	if p.SampleRate() != 44100 {
		t.Errorf("SampleRate() = %d, want 44100", p.SampleRate())
	}
}

func TestStreamTitleEmpty(t *testing.T) {
	p := newTestPlayer()
	if got := p.StreamTitle(); got != "" {
		t.Errorf("StreamTitle() on fresh player = %q, want empty", got)
	}
}

func TestSetStreamTitle(t *testing.T) {
	p := newTestPlayer()
	p.setStreamTitle("Artist - Song")
	if got := p.StreamTitle(); got != "Artist - Song" {
		t.Errorf("StreamTitle() = %q, want 'Artist - Song'", got)
	}
}

func TestRegisterStreamerFactory(t *testing.T) {
	p := newTestPlayer()
	var noop StreamerFactory // nil factory is fine for this storage-only test
	p.RegisterStreamerFactory("spotify:", noop)
	p.mu.Lock()
	defer p.mu.Unlock()
	if len(p.customFactories) != 1 {
		t.Errorf("len(customFactories) = %d, want 1", len(p.customFactories))
	}
	if _, ok := p.customFactories["spotify:"]; !ok {
		t.Error("factory not stored under 'spotify:'")
	}
}

func TestRegisterBufferedURLMatcher(t *testing.T) {
	p := newTestPlayer()
	match := func(u string) bool { return u == "foo" }
	p.RegisterBufferedURLMatcher(match)
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.bufferedURLMatch == nil {
		t.Error("matcher not stored")
	}
}

func TestConcurrentVolumeSetRead(t *testing.T) {
	p := newTestPlayer()
	var wg sync.WaitGroup
	const N = 100

	wg.Add(N * 2)
	for i := 0; i < N; i++ {
		go func(i int) {
			defer wg.Done()
			p.SetVolume(float64(-i % 30))
		}(i)
		go func() {
			defer wg.Done()
			_ = p.Volume()
		}()
	}
	wg.Wait()
	// If the race detector didn't flag anything, we're happy.
}

// Ensure the atomic uint64 storage actually wraps dB as expected.
func TestVolumeStoragePrecision(t *testing.T) {
	p := newTestPlayer()
	p.SetVolume(-3.14)
	v := math.Float64frombits(p.volume.Load())
	if math.Abs(v-(-3.14)) > 1e-12 {
		t.Errorf("stored volume = %v, want -3.14", v)
	}
}
