package cmd

import (
	"context"
	"io"
	"math"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"

	"github.com/steipete/sag/internal/elevenlabs"
)

func TestInferFormatFromExt(t *testing.T) {
	tests := []struct {
		path string
		want string
	}{
		{"out.mp3", "mp3_44100_128"},
		{"out.MP3", "mp3_44100_128"},
		{"audio.wav", "pcm_44100"},
		{"audio.WAVE", "pcm_44100"},
		{"audio.unknown", ""},
	}
	for _, tt := range tests {
		if got := inferFormatFromExt(tt.path); got != tt.want {
			t.Fatalf("inferFormatFromExt(%q) = %q, want %q", tt.path, got, tt.want)
		}
	}
}

func TestResolveTextFromArgs(t *testing.T) {
	got, err := resolveText([]string{"hello", "world"}, "")
	if err != nil {
		t.Fatalf("resolveText args error: %v", err)
	}
	if got != "hello world" {
		t.Fatalf("resolveText args = %q, want %q", got, "hello world")
	}
}

func TestResolveTextFromFile(t *testing.T) {
	tmp, err := os.CreateTemp("", "sag_text")
	if err != nil {
		t.Fatalf("temp file: %v", err)
	}
	defer func() { _ = os.Remove(tmp.Name()) }()
	if _, err := tmp.WriteString("from file"); err != nil {
		t.Fatalf("write temp: %v", err)
	}
	if err := tmp.Close(); err != nil {
		t.Fatalf("close temp: %v", err)
	}

	got, err := resolveText(nil, tmp.Name())
	if err != nil {
		t.Fatalf("resolveText file error: %v", err)
	}
	if got != "from file" {
		t.Fatalf("resolveText file = %q, want %q", got, "from file")
	}
}

func TestResolveTextFromStdin(t *testing.T) {
	orig := os.Stdin
	defer func() { os.Stdin = orig }()

	r, w, err := os.Pipe()
	if err != nil {
		t.Fatalf("pipe: %v", err)
	}
	if _, err := w.WriteString("from stdin"); err != nil {
		t.Fatalf("write pipe: %v", err)
	}
	if err := w.Close(); err != nil {
		t.Fatalf("close pipe writer: %v", err)
	}
	os.Stdin = r

	got, err := resolveText(nil, "")
	if err != nil {
		t.Fatalf("resolveText stdin error: %v", err)
	}
	if got != "from stdin" {
		t.Fatalf("resolveText stdin = %q, want %q", got, "from stdin")
	}
}

func TestResolveTextFileNotFound(t *testing.T) {
	if _, err := resolveText(nil, "/tmp/does-not-exist-sag"); err == nil {
		t.Fatalf("expected error for missing file")
	}
}

func TestResolveTextEmptySources(t *testing.T) {
	// With no args, no file, and stdin still a TTY, expect an error.
	if _, err := resolveText(nil, ""); err == nil {
		t.Fatalf("expected error when no text is provided")
	}
}

func TestResolveTextEmptyFile(t *testing.T) {
	tmp, err := os.CreateTemp("", "sag_empty")
	if err != nil {
		t.Fatalf("temp file: %v", err)
	}
	defer func() { _ = os.Remove(tmp.Name()) }()

	if err := tmp.Close(); err != nil {
		t.Fatalf("close temp: %v", err)
	}

	if _, err := resolveText(nil, tmp.Name()); err == nil {
		t.Fatalf("expected error on empty input file")
	}
}

func TestApplyRateOverridesInvalidSpeed(t *testing.T) {
	opts := &speakOptions{speed: 0.3, rateWPM: 200}
	if err := applyRateAndSpeed(opts); err != nil {
		t.Fatalf("applyRateAndSpeed error: %v", err)
	}
	want := float64(200) / float64(defaultWPM)
	if math.Abs(opts.speed-want) > 1e-9 {
		t.Fatalf("expected speed %.2f, got %.2f", want, opts.speed)
	}
}

func TestApplyRateAndSpeedInvalidSpeed(t *testing.T) {
	opts := &speakOptions{speed: 0.3}
	if err := applyRateAndSpeed(opts); err == nil {
		t.Fatalf("expected speed validation error")
	}
}

func TestResolveVoiceDefaultsToFirst(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if _, err := w.Write([]byte(`{"voices":[{"voice_id":"id1","name":"Alpha","category":"premade"},{"voice_id":"id2","name":"Beta","category":"premade"}]}`)); err != nil {
			t.Fatalf("write response: %v", err)
		}
	}))
	defer srv.Close()

	client := elevenlabs.NewClient("key", srv.URL)
	id, err := resolveVoice(context.Background(), client, "", false)
	if err != nil {
		t.Fatalf("resolveVoice error: %v", err)
	}
	if id != "id1" {
		t.Fatalf("resolveVoice default id = %q, want id1", id)
	}
}

func TestResolveVoicePassThroughIDWithDigits(t *testing.T) {
	// Should short-circuit without hitting the server when input looks like an ID with digits.
	srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
		t.Fatalf("server should not be called for ID pass-through")
	}))
	defer srv.Close()

	client := elevenlabs.NewClient("key", srv.URL)
	id, err := resolveVoice(context.Background(), client, "abc1234567890123", false)
	if err != nil {
		t.Fatalf("resolveVoice error: %v", err)
	}
	if id != "abc1234567890123" {
		t.Fatalf("expected ID to pass through, got %q", id)
	}
}

func TestResolveVoiceForceIDPassThrough(t *testing.T) {
	// Should short-circuit without hitting the server when --voice-id is set.
	srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
		t.Fatalf("server should not be called for forced ID pass-through")
	}))
	defer srv.Close()

	client := elevenlabs.NewClient("key", srv.URL)
	input := "OnlyLettersVoiceID"
	id, err := resolveVoice(context.Background(), client, input, true)
	if err != nil {
		t.Fatalf("resolveVoice error: %v", err)
	}
	if id != input {
		t.Fatalf("expected ID to pass through, got %q", id)
	}
}

func TestResolveVoiceLongNameExactMatch(t *testing.T) {
	var called bool
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		called = true
		if _, err := w.Write([]byte(`{"voices":[{"voice_id":"id-long","name":"LongVoiceNameAlpha","category":"premade"}]}`)); err != nil {
			t.Fatalf("write response: %v", err)
		}
	}))
	defer srv.Close()

	client := elevenlabs.NewClient("key", srv.URL)
	id, err := resolveVoice(context.Background(), client, "LongVoiceNameAlpha", false)
	if err != nil {
		t.Fatalf("resolveVoice error: %v", err)
	}
	if !called {
		t.Fatalf("expected voice lookup for long name")
	}
	if id != "id-long" {
		t.Fatalf("expected id-long, got %q", id)
	}
}

func TestResolveVoiceLooksLikeIDNoMatchPassesThrough(t *testing.T) {
	var called bool
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		called = true
		if _, err := w.Write([]byte(`{"voices":[{"voice_id":"id1","name":"Other","category":"premade"}]}`)); err != nil {
			t.Fatalf("write response: %v", err)
		}
	}))
	defer srv.Close()

	client := elevenlabs.NewClient("key", srv.URL)
	input := "LongVoiceNameAlpha"
	id, err := resolveVoice(context.Background(), client, input, false)
	if err != nil {
		t.Fatalf("resolveVoice error: %v", err)
	}
	if !called {
		t.Fatalf("expected voice lookup for ambiguous input")
	}
	if id != input {
		t.Fatalf("expected %q to pass through, got %q", input, id)
	}
}

func TestResolveVoiceNoMatch(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if _, err := w.Write([]byte(`{"voices":[{"voice_id":"id1","name":"Near","category":"premade"}]}`)); err != nil {
			t.Fatalf("write response: %v", err)
		}
	}))
	defer srv.Close()

	client := elevenlabs.NewClient("key", srv.URL)
	_, err := resolveVoice(context.Background(), client, "nothing-match", false)
	if err == nil {
		t.Fatalf("expected error for non-matching voice")
	}
	if !strings.Contains(err.Error(), "not found") {
		t.Fatalf("expected 'not found' error, got %q", err.Error())
	}
}

func TestResolveVoicePartialMatch(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if _, err := w.Write([]byte(`{"voices":[{"voice_id":"id1","name":"Sarah","category":"premade"},{"voice_id":"id2","name":"Roger - Casual","category":"premade"}]}`)); err != nil {
			t.Fatalf("write response: %v", err)
		}
	}))
	defer srv.Close()

	restore, read := captureStderr(t)
	defer restore()

	client := elevenlabs.NewClient("key", srv.URL)
	id, err := resolveVoice(context.Background(), client, "roger", false)
	if err != nil {
		t.Fatalf("resolveVoice error: %v", err)
	}
	if id != "id2" {
		t.Fatalf("expected id2 for partial match 'roger', got %q", id)
	}
	if out := read(); !strings.Contains(out, "using voice") {
		t.Fatalf("expected 'using voice' notice, got %q", out)
	}
}

func TestResolveVoiceListOutputsTable(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if _, err := w.Write([]byte(`{"voices":[{"voice_id":"id1","name":"Alpha","category":"premade"}]}`)); err != nil {
			t.Fatalf("write response: %v", err)
		}
	}))
	defer srv.Close()

	restore, read := captureStdout(t)
	defer restore()

	client := elevenlabs.NewClient("key", srv.URL)
	id, err := resolveVoice(context.Background(), client, "?", false)
	if err != nil {
		t.Fatalf("resolveVoice error: %v", err)
	}
	if id != "" {
		t.Fatalf("expected empty ID when listing voices, got %q", id)
	}
	if out := read(); !strings.Contains(out, "VOICE ID") || !strings.Contains(out, "Alpha") {
		t.Fatalf("expected table output, got %q", out)
	}
}

func TestStreamAndPlayWritesOutput(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !strings.Contains(r.URL.Path, "/stream") {
			t.Fatalf("unexpected path: %s", r.URL.Path)
		}
		_, _ = w.Write([]byte("stream-bytes"))
	}))
	defer srv.Close()

	client := elevenlabs.NewClient("key", srv.URL)
	tmp := t.TempDir()
	out := tmp + "/out.mp3"
	opts := speakOptions{voiceID: "v1", outputPath: out, stream: true, play: false}
	payload := elevenlabs.TTSRequest{Text: "hi"}

	if _, err := streamAndPlay(context.Background(), client, opts, payload); err != nil {
		t.Fatalf("streamAndPlay error: %v", err)
	}
	data, err := os.ReadFile(out)
	if err != nil {
		t.Fatalf("read output: %v", err)
	}
	if string(data) != "stream-bytes" {
		t.Fatalf("unexpected output data: %q", string(data))
	}
}

func TestConvertAndPlayWritesOutput(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !strings.Contains(r.URL.Path, "/text-to-speech/") {
			t.Fatalf("unexpected path: %s", r.URL.Path)
		}
		_, _ = w.Write([]byte("convert-bytes"))
	}))
	defer srv.Close()

	client := elevenlabs.NewClient("key", srv.URL)
	tmp := t.TempDir()
	out := tmp + "/out.mp3"
	opts := speakOptions{voiceID: "v1", outputPath: out, play: false}
	payload := elevenlabs.TTSRequest{Text: "hi"}

	if _, err := convertAndPlay(context.Background(), client, opts, payload); err != nil {
		t.Fatalf("convertAndPlay error: %v", err)
	}
	data, err := os.ReadFile(out)
	if err != nil {
		t.Fatalf("read output: %v", err)
	}
	if string(data) != "convert-bytes" {
		t.Fatalf("unexpected output data: %q", string(data))
	}
}

func TestStreamAndPlayRequiresWork(t *testing.T) {
	client := elevenlabs.NewClient("key", "http://invalid")
	opts := speakOptions{voiceID: "v1", play: false, stream: true}
	payload := elevenlabs.TTSRequest{Text: "hi"}

	_, err := streamAndPlay(context.Background(), client, opts, payload)
	if err == nil {
		t.Fatalf("expected error when no output and play disabled")
	}
}

func TestStreamAndPlayWithPlayback(t *testing.T) {
	called := false
	restore := stubPlay(t, func(data []byte) {
		called = true
		if string(data) != "stream-play" {
			t.Fatalf("unexpected data: %q", string(data))
		}
	})
	defer restore()

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte("stream-play"))
	}))
	defer srv.Close()

	client := elevenlabs.NewClient("key", srv.URL)
	opts := speakOptions{voiceID: "v1", play: true, stream: true}
	payload := elevenlabs.TTSRequest{Text: "hi"}

	if _, err := streamAndPlay(context.Background(), client, opts, payload); err != nil {
		t.Fatalf("streamAndPlay error: %v", err)
	}
	if !called {
		t.Fatalf("expected playback to be invoked")
	}
}

func TestConvertAndPlayWithPlayback(t *testing.T) {
	called := false
	restore := stubPlay(t, func(data []byte) {
		called = true
		if string(data) != "convert-play" {
			t.Fatalf("unexpected data: %q", string(data))
		}
	})
	defer restore()

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte("convert-play"))
	}))
	defer srv.Close()

	client := elevenlabs.NewClient("key", srv.URL)
	opts := speakOptions{voiceID: "v1", play: true, outputPath: "", stream: false}
	payload := elevenlabs.TTSRequest{Text: "hi"}

	if _, err := convertAndPlay(context.Background(), client, opts, payload); err != nil {
		t.Fatalf("convertAndPlay error: %v", err)
	}
	if !called {
		t.Fatalf("expected playback to be invoked")
	}
}

func captureStdout(t *testing.T) (restore func(), read func() string) {
	t.Helper()
	orig := os.Stdout
	r, w, err := os.Pipe()
	if err != nil {
		t.Fatalf("pipe: %v", err)
	}
	os.Stdout = w
	return func() {
			_ = w.Close()
			os.Stdout = orig
		}, func() string {
			_ = w.Close()
			b, _ := io.ReadAll(r)
			return string(b)
		}
}

func captureStderr(t *testing.T) (restore func(), read func() string) {
	t.Helper()
	orig := os.Stderr
	r, w, err := os.Pipe()
	if err != nil {
		t.Fatalf("pipe: %v", err)
	}
	os.Stderr = w
	return func() {
			_ = w.Close()
			os.Stderr = orig
		}, func() string {
			_ = w.Close()
			b, _ := io.ReadAll(r)
			return string(b)
		}
}

func TestResolveVoiceByName(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if _, err := w.Write([]byte(`{"voices":[{"voice_id":"id-sarah","name":"Sarah","category":"premade"},{"voice_id":"id-roger","name":"Roger","category":"premade"}]}`)); err != nil {
			t.Fatalf("write response: %v", err)
		}
	}))
	defer srv.Close()

	client := elevenlabs.NewClient("key", srv.URL)
	id, err := resolveVoice(context.Background(), client, "roger", false)
	if err != nil {
		t.Fatalf("resolveVoice error: %v", err)
	}
	if id != "id-roger" {
		t.Fatalf("resolveVoice by name = %q, want id-roger", id)
	}
}

func stubPlay(t *testing.T, fn func([]byte)) func() {
	t.Helper()
	orig := playToSpeakers
	playToSpeakers = func(_ context.Context, r io.Reader) error {
		b, _ := io.ReadAll(r)
		fn(b)
		return nil
	}
	return func() { playToSpeakers = orig }
}
