// tts_test.go — sesli çıkış (Faz 7) uçtan uca testleri.
//
// Kapsam: tüm kareler sahte (fakeConn), LLM sahte (fakeLLM), TTS motoru sahte (fakeTTS).
// Amaç: "sesli mod açık → cümle cümle ses gönderimi → kesme/barge-in" zincirini
// gerçek eşzamanlı kod yoluyla doğrulamak.
package ws

import (
	"context"
	"encoding/json"
	"strings"
	"testing"
	"time"

	"fiko-ai/internal/agent"
	"fiko-ai/internal/conversation"
	"fiko-ai/internal/llm"
	"fiko-ai/internal/protocol"
	"fiko-ai/internal/tts"
)

// fakeTTS metni sabit ses baytlarına çevirir.
type fakeTTS struct {
	audio []byte
	mime  string
	err   error
	got   []string
	calls int
	block chan struct{} // çağrılar burada bloklanır (barge-in testi)
}

func (f *fakeTTS) Speak(ctx context.Context, text string) (tts.Speech, error) {
	f.calls++
	f.got = append(f.got, text)
	if f.block != nil {
		select {
		case <-f.block:
		case <-ctx.Done():
			return tts.Speech{}, ctx.Err()
		}
	}
	if f.err != nil {
		return tts.Speech{}, f.err
	}
	mime := f.mime
	if mime == "" {
		mime = "audio/mpeg"
	}
	return tts.Speech{Audio: f.audio, Mime: mime}, nil
}

// newTTSFixture sesli çıkış bağlı hub kurar (STT yok: yalnız TTS yolu test edilir).
func newTTSFixture(t *testing.T, f *fakeLLM, syn tts.Synthesizer) *fixture {
	t.Helper()
	svc := conversation.NewService(conversation.NewMemoryRepo())
	c, err := svc.Create(context.Background(), "u")
	if err != nil {
		t.Fatal(err)
	}
	log := testLogger()
	ag := agent.New(svc, f, nil, nil, log, agent.Config{})
	hub := NewHubWithVoice(ag, nil, syn, log, Options{})
	conn := newFakeConn()
	fx := &fixture{hub: hub, ag: ag, svc: svc, conv: c.ID, conn: conn, llm: f, done: make(chan struct{})}
	go func() { defer close(fx.done); hub.Serve(context.Background(), conn, "u") }()
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel()
		_ = ag.Close(ctx)
		_ = hub.Close(ctx)
		select {
		case <-fx.done:
		case <-time.After(3 * time.Second):
		}
	})
	return fx
}

func ttsMode(t *testing.T, fx *fixture, id string, on bool) {
	t.Helper()
	fx.conn.in <- frame(t, id, protocol.ClientTTSMode, fx.conv,
		protocol.TTSModePayload{ConversationID: fx.conv, Enabled: on})
}

func ask(t *testing.T, fx *fixture, id, text string) {
	t.Helper()
	fx.conn.in <- frame(t, id, protocol.ClientTextMessage, fx.conv, protocol.TextMessagePayload{Text: text})
}

// frame, konuşma kimliğini KARE ÜZERİNDE taşıyan test karesi üretir.
//
// Neden gerekli? Dosyadaki `inbound` yardımcısı ConversationID'yi doldurmaz.
// `voice.*` olayları kimliği payload'dan okur (VoiceStopPayload.ConversationID),
// ama `text.message` payload'ında alan YOKTUR; kimlik yalnızca kare üzerinden gelir.
// (session.go'daki voiceConvID yorumu bu yüzden yazılmıştı.)
func frame(t *testing.T, id string, typ protocol.ClientType, convID string, payload any) []byte {
	t.Helper()
	raw, err := json.Marshal(payload)
	if err != nil {
		t.Fatal(err)
	}
	b, err := json.Marshal(protocol.Inbound{Type: typ, ID: id, ConversationID: convID, Payload: raw})
	if err != nil {
		t.Fatal(err)
	}
	return b
}

// TestTTS_ModeOnSpeaksSentences: sesli mod açıkken yanıt cümle cümle seslenir.
func TestTTS_ModeOnSpeaksSentences(t *testing.T) {
	f := &fakeLLM{stream: textStream("Merhaba. Nasılsın?")}
	syn := &fakeTTS{audio: []byte("SES")}
	fx := newTTSFixture(t, f, syn)

	ttsMode(t, fx, "m1", true)
	ask(t, fx, "m2", "selam")

	for i := 0; i < 2; i++ {
		o := waitForType(t, fx, protocol.ServerTTSAudio)
		p, ok := o.Payload.(map[string]any)
		if !ok {
			t.Fatalf("tts.audio payload harita değil: %T", o.Payload)
		}
		if p["mime"] != "audio/mpeg" || p["data"] == nil || p["data"] == "" {
			t.Errorf("tts.audio eksik: mime=%v data=%v", p["mime"], p["data"])
		}
	}
	o := waitForType(t, fx, protocol.ServerTTSDone)
	pd := o.Payload.(map[string]any)
	if pd["sentences"] != float64(2) {
		t.Errorf("sentences = %v, beklenen 2", pd["sentences"])
	}
	if len(syn.got) != 2 {
		t.Fatalf("motor cümle sayısı = %d (%v)", len(syn.got), syn.got)
	}
	if !strings.Contains(syn.got[0], "Merhaba") || !strings.Contains(syn.got[1], "Nasıl") {
		t.Errorf("motor cümleleri = %v", syn.got)
	}
}

// TestTTS_StreamsDuringTextFlow (GERÇEK ZAMAN davranışı): ses cümle TAMAMLANDIĞINDA,
// yani metin AKIŞI sırasında gönderilir — text.final'i beklemez.
//
// Neden önemli? Önceki davranışta seslendirme yalnızca `text.final` sonrası başlardı;
// kullanıcı cevabın tamamını okuduktan ~5-8 sn sonra ilk sesi duyardu. Bu test,
// ilk cümlenin "final" gelmeden gittiğini kanıtlar.
//
// DİKKAT: textStream TEK delta yollar; gerçek akış parça parça gelir. Bu yüzden
// akışı ayrıca kurup 2. cümle üretilirken 1. cümlenin sesinin çıktığını gösteriyoruz.
// `gate` kanalı 2. cümleyi BİLEK tutar: ses ondan önce gelirse akışlı çalışıyor demektir.
func TestTTS_StreamsDuringTextFlow(t *testing.T) {
	gate := make(chan struct{}) // 2. cümleyi bekletir
	defer close(gate)
	halves := []string{"Merhaba. ", "Nasılsın?"}
	idx := 0
	stream := func(ctx context.Context, _ llm.Request, on llm.DeltaFunc) (llm.Result, error) {
		var sb strings.Builder
		for idx < len(halves) {
			d := halves[idx]
			idx++
			sb.WriteString(d)
			if on != nil {
				if err := on(d); err != nil {
					return llm.Result{}, err
				}
			}
			if idx == 1 {
				// İlk cümle YAYINLANDI. Şimdi dur: gerçek akışta da sunucu burada
				// ikinci cümleyi üretmeye devam ederken TTS'i çalıştırıyor olmalı.
				// Gate'i biz (test) açacağız; bu arada "final" gelirse akışlı
				// seslendirme çalışmıyor demektir.
				<-gate
			}
		}
		return llm.Result{Content: sb.String()}, nil
	}

	f := &fakeLLM{stream: stream}
	syn := &fakeTTS{audio: []byte("SES")}
	fx := newTTSFixture(t, f, syn)

	ttsMode(t, fx, "m1", true)
	ask(t, fx, "m2", "selam")

	// Teşhis: akış gerçekten iki parça mı veriyor, yoksa tek delta mı?
	// (textStream tek delta verir; akışlı seslendirmenin kanıtı buna bağlı.)
	time.Sleep(50 * time.Millisecond)

	// text.final GELMEDEN ilk ses duyulmalı (gate hâlâ kapalıyken).
	deadline := time.After(5 * time.Second)
	sawAudio, sawFinal := false, false
	for !sawAudio || sawFinal {
		select {
		case b := <-fx.conn.writes:
			var o protocol.Outbound
			if err := json.Unmarshal(b, &o); err != nil {
				continue
			}
			switch o.Type {
			case protocol.ServerTTSAudio:
				sawAudio = true
			case protocol.ServerAssistantTextFinal:
				sawFinal = true
			}
			if sawAudio {
				// Ses "final"dan ÖNCE geldiyse akışlı seslendirme kanıtlandı.
				if sawFinal {
					t.Fatal("ses ancak text.final SONRASINDAN geldi (akışlı değil)")
				}
				return // kanıtlandı
			}
		case <-deadline:
			if sawFinal {
				t.Fatal("text.final geldi ama ses yok")
			}
			t.Fatal("text.final beklenmeden tts.audio gelmedi (akışlı seslendirme çalışmıyor)")
		}
	}
}

// TestTTS_LastSentenceWithoutPunctuation: noktalama olmayan son parça da seslenir.
//
// Neden? Akışlı üretimde son delta'da noktalama gelmeyebilir ("Tamam." yerine "Tamam").
// O metin sessizce kaybolursa cevap eksik duyulur; finishSpeech onu kuyruğa alır.
func TestTTS_LastSentenceWithoutPunctuation(t *testing.T) {
	f := &fakeLLM{stream: textStream("Tamam")}
	syn := &fakeTTS{audio: []byte("SES")}
	fx := newTTSFixture(t, f, syn)

	ttsMode(t, fx, "m1", true)
	ask(t, fx, "m2", "selam")

	waitForType(t, fx, protocol.ServerTTSAudio)
	waitForType(t, fx, protocol.ServerTTSDone)
	if len(syn.got) != 1 {
		t.Fatalf("noktalamasız son cümle seslenmedi: %v", syn.got)
	}
	if !strings.Contains(syn.got[0], "Tamam") {
		t.Errorf("seslenen metin = %q", syn.got[0])
	}
}

// TestTTS_DecimalNotSplit: "3.14" gibi metin yanlışlıkla cümle bölünmez.
func TestTTS_DecimalNotSplit(t *testing.T) {
	f := &fakeLLM{stream: textStream("Pi sayısı 3.14 eder. Bitti.")}
	syn := &fakeTTS{audio: []byte("SES")}
	fx := newTTSFixture(t, f, syn)

	ttsMode(t, fx, "m1", true)
	ask(t, fx, "m2", "selam")

	waitForType(t, fx, protocol.ServerTTSDone)
	if len(syn.got) != 2 {
		t.Fatalf("cümle sayısı = %d (%v), beklenen 2", len(syn.got), syn.got)
	}
	if !strings.Contains(syn.got[0], "3.14") {
		t.Errorf("ondalık sayı bölündü: %q", syn.got[0])
	}
}

// TestTTS_ModeOffStaysSilent: sesli mod kapalıyken motor HİÇ çağrılmaz.
func TestTTS_ModeOffStaysSilent(t *testing.T) {
	f := &fakeLLM{stream: textStream("Merhaba.")}
	syn := &fakeTTS{audio: []byte("SES")}
	fx := newTTSFixture(t, f, syn)

	ask(t, fx, "m2", "selam")
	waitForType(t, fx, protocol.ServerAssistantTextFinal)
	time.Sleep(150 * time.Millisecond)

	if syn.calls != 0 {
		t.Fatalf("sesli mod kapalıyken motor %d kez çağrıldı", syn.calls)
	}
}

// TestTTS_NotConfigured: motor yoksa tts.mode açık hata döner (sessizce yutmaz).
func TestTTS_NotConfigured(t *testing.T) {
	f := &fakeLLM{stream: textStream("Merhaba.")}
	fx := newTTSFixture(t, f, nil)

	ttsMode(t, fx, "m1", true)

	o := waitForType(t, fx, protocol.ServerError)
	pe, ok := o.Payload.(map[string]any)
	if !ok {
		t.Fatalf("error payload harita değil: %T", o.Payload)
	}
	if pe["code"] != "not_implemented" {
		t.Errorf("kod = %v, beklenen not_implemented", pe["code"])
	}
}

// waitForDone, stopped bayrağı beklenen değer olan tts.done olayını bekler.
//
// Neden özel? tts.done İKİ yerden gelir: (1) seslendirme normal bitti (stopped=false),
// (2) kullanıcı kesti (stopped=true). Hangiinin önce döneceği ZAMANLAMAYA bağlı olduğu
// için ilk tts.done'u almak kararsızdı (test ilk koşuda geçti, ikincisinde düştü).
func waitForDone(t *testing.T, fx *fixture, wantStopped bool) map[string]any {
	t.Helper()
	deadline := time.After(5 * time.Second)
	for {
		select {
		case b := <-fx.conn.writes:
			var o protocol.Outbound
			if err := json.Unmarshal(b, &o); err != nil || o.Type != protocol.ServerTTSDone {
				continue
			}
			pd, ok := o.Payload.(map[string]any)
			if !ok {
				continue
			}
			if pd["stopped"] == wantStopped {
				return pd
			}
		case <-deadline:
			t.Fatalf("stopped=%v olan tts.done gelmedi", wantStopped)
		}
	}
}

// TestTTS_StopCutsOff: tts.stop üretimi keser, tts.done(stopped) döner.
func TestTTS_StopCutsOff(t *testing.T) {
	f := &fakeLLM{stream: textStream("Bir. İki. Üç.")}
	block := make(chan struct{})
	syn := &fakeTTS{audio: []byte("SES"), block: block}
	fx := newTTSFixture(t, f, syn)
	defer close(block)

	ttsMode(t, fx, "m1", true)
	ask(t, fx, "m2", "selam")

	waitForType(t, fx, protocol.ServerAssistantTextFinal)
	time.Sleep(100 * time.Millisecond)
	fx.conn.in <- frame(t, "s1", protocol.ClientTTSStop, fx.conv, protocol.TTSStopPayload{ConversationID: fx.conv})

	pd := waitForDone(t, fx, true)
	if pd["stopped"] != true {
		t.Errorf("stopped = %v, beklenen true", pd["stopped"])
	}
	time.Sleep(150 * time.Millisecond)
	if syn.calls > 2 {
		t.Errorf("kesildikten sonra motor hâlâ çağrıldı: %d", syn.calls)
	}
}

// TestTTS_VoiceStartBargeIn: mikrofon açılınca asstan susar.
func TestTTS_VoiceStartBargeIn(t *testing.T) {
	f := &fakeLLM{stream: textStream("Bir. İki.")}
	block := make(chan struct{})
	syn := &fakeTTS{audio: []byte("SES"), block: block}
	fx := newTTSFixture(t, f, syn)
	defer close(block)

	ttsMode(t, fx, "m1", true)
	ask(t, fx, "m2", "selam")
	waitForType(t, fx, protocol.ServerAssistantTextFinal)
	time.Sleep(100 * time.Millisecond)

	// STT yapılandırılmamış olabilir; önemli olan barge-in'in TTS'i kesmesidir.
	fx.conn.in <- frame(t, "v1", protocol.ClientVoiceStart, fx.conv,
		protocol.VoiceStartPayload{ConversationID: fx.conv, Mime: "audio/webm"})

	pd := waitForDone(t, fx, true)
	if pd["stopped"] != true {
		t.Errorf("barge-in: stopped = %v, beklenen true", pd["stopped"])
	}
}

// TestTTS_MarkdownCleaned: markdown temizlenmiş metin motora gider.
func TestTTS_MarkdownCleaned(t *testing.T) {
	f := &fakeLLM{stream: textStream("**Kalın** metin.")}
	syn := &fakeTTS{audio: []byte("SES")}
	fx := newTTSFixture(t, f, syn)

	ttsMode(t, fx, "m1", true)
	ask(t, fx, "m2", "selam")

	waitForType(t, fx, protocol.ServerTTSAudio)
	waitForType(t, fx, protocol.ServerTTSDone)
	if len(syn.got) == 0 || syn.got[0] != "Kalın metin." {
		t.Errorf("motor metni = %v, beklenen 'Kalın metin.'", syn.got)
	}
}

// TestTTS_EngineErrorIsReported: motor hatası sessizce yutulmaz.
func TestTTS_EngineErrorIsReported(t *testing.T) {
	f := &fakeLLM{stream: textStream("Merhaba.")}
	syn := &fakeTTS{err: tts.ErrUnavailable}
	fx := newTTSFixture(t, f, syn)

	ttsMode(t, fx, "m1", true)
	ask(t, fx, "m2", "selam")

	o := waitForType(t, fx, protocol.ServerError)
	pe, _ := o.Payload.(map[string]any)
	if pe["retryable"] != true {
		t.Errorf("retryable = %v, beklenen true", pe["retryable"])
	}
}

// TestTTS_CodeOnlyAnswerIsSilent: yalnız kod bloğu içeren yanıt seslenmez (hata da değil).
func TestTTS_CodeOnlyAnswerIsSilent(t *testing.T) {
	f := &fakeLLM{stream: textStream("```go\nfmt.Println(1)\n```")}
	syn := &fakeTTS{audio: []byte("SES")}
	fx := newTTSFixture(t, f, syn)

	ttsMode(t, fx, "m1", true)
	ask(t, fx, "m2", "kod yaz")

	waitForType(t, fx, protocol.ServerAssistantTextFinal)
	time.Sleep(150 * time.Millisecond)
	if syn.calls != 0 {
		t.Errorf("yalnız kod yanıtında motor çağrıldı: %d", syn.calls)
	}
}
