package voicesink

import (
	"context"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"strings"
	"testing"
	"time"

	"github.com/kami/maven/internal/audio"
	"github.com/kami/maven/internal/delivery"
	"github.com/kami/maven/internal/tts"
	"github.com/kami/maven/internal/voice"
)

type frameResult struct {
	buf []byte
	err error
}

func readFrame(r net.Conn, ch chan<- frameResult) {
	var hdr [4]byte
	if _, err := io.ReadFull(r, hdr[:]); err != nil {
		ch <- frameResult{err: err}
		return
	}
	n := binary.BigEndian.Uint32(hdr[:])
	buf := make([]byte, n)
	if _, err := io.ReadFull(r, buf); err != nil {
		ch <- frameResult{err: err}
		return
	}
	ch <- frameResult{buf: buf}
}

func unmarshalFrame(t *testing.T, buf []byte) (kind string, params json.RawMessage) {
	t.Helper()
	var frame struct {
		Kind   string          `json:"kind"`
		Params json.RawMessage `json:"p"`
	}
	if err := json.Unmarshal(buf, &frame); err != nil {
		t.Fatalf("unmarshal frame: %v", err)
	}
	return frame.Kind, frame.Params
}

func unmarshalPush(t *testing.T, raw json.RawMessage) voice.AudioNudgePush {
	t.Helper()
	var push voice.AudioNudgePush
	if err := json.Unmarshal(raw, &push); err != nil {
		t.Fatalf("unmarshal push params: %v", err)
	}
	return push
}

// ---- nil guards ----

func TestSinkSend_NilTTSError(t *testing.T) {
	s := New(nil, voice.NewSessions())
	err := s.Send(context.Background(), delivery.Sendable{Body: "test"})
	if err == nil || !strings.Contains(err.Error(), "tts synthesizer not wired") {
		t.Fatalf("expected tts-not-wired error, got %v", err)
	}
}

func TestSinkSend_NilSessionsError(t *testing.T) {
	s := New(tts.NewStub(), nil)
	err := s.Send(context.Background(), delivery.Sendable{Body: "test"})
	if err == nil || !strings.Contains(err.Error(), "sessions registry not wired") {
		t.Fatalf("expected sessions-not-wired error, got %v", err)
	}
}

// ---- no session ----

func TestSinkSend_NoSessionReturnsErrVoiceNoSession(t *testing.T) {
	sess := voice.NewSessions()
	s := New(tts.NewStub(), sess)
	err := s.Send(context.Background(), delivery.Sendable{Body: "hello"})
	if !errors.Is(err, delivery.ErrVoiceNoSession) {
		t.Fatalf("expected ErrVoiceNoSession, got %v", err)
	}
}

// ---- successful push ----

func TestSinkSend_WithSessionPushesAudio(t *testing.T) {
	r, w := net.Pipe()
	defer r.Close()
	defer w.Close()

	ch := make(chan frameResult, 1)
	go readFrame(r, ch)

	ctx := context.Background()
	sess := voice.NewSessions()
	sess.Add(w, voice.SurfacePCClient)
	s := New(tts.NewStub(), sess)

	err := s.Send(ctx, delivery.Sendable{
		RuleName: "test_rule",
		Body:     "Hello world",
		Kind:     delivery.KindNudge,
		Ts:       time.Now(),
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	res := <-ch
	if res.err != nil {
		t.Fatalf("read frame: %v", res.err)
	}

	kind, params := unmarshalFrame(t, res.buf)
	if kind != "audio_nudge" {
		t.Errorf("push kind = %q, want %q", kind, "audio_nudge")
	}

	push := unmarshalPush(t, params)
	if push.RuleName != "test_rule" {
		t.Errorf("rule_name = %q, want %q", push.RuleName, "test_rule")
	}
	if push.Text != "Hello world" {
		t.Errorf("text = %q, want %q", push.Text, "Hello world")
	}
	if push.Audio.Format != audio.PCM16kMono {
		t.Errorf("audio format = %v, want %v", push.Audio.Format, audio.PCM16kMono)
	}
	if len(push.Audio.Bytes) == 0 {
		t.Error("audio bytes are empty")
	}
}

// ---- empty body falls back to summary ----

func TestSinkSend_EmptyBodyFallsBackToSummary(t *testing.T) {
	r, w := net.Pipe()
	defer r.Close()
	defer w.Close()

	ch := make(chan frameResult, 1)
	go readFrame(r, ch)

	sess := voice.NewSessions()
	sess.Add(w, voice.SurfacePCClient)
	s := New(tts.NewStub(), sess)

	err := s.Send(context.Background(), delivery.Sendable{
		Body:    "",
		Summary: "Fallback summary",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	res := <-ch
	if res.err != nil {
		t.Fatalf("read frame: %v", res.err)
	}

	_, params := unmarshalFrame(t, res.buf)
	push := unmarshalPush(t, params)
	if push.Text != "Fallback summary" {
		t.Errorf("text = %q, want %q", push.Text, "Fallback summary")
	}
}

// ---- fake synths ----

type errSynth struct{}

func (e *errSynth) Synthesize(_ context.Context, text string) (audio.Audio, error) {
	return audio.Audio{}, fmt.Errorf("synthesis failed")
}

func TestSinkSend_SynthesizeError(t *testing.T) {
	sess := voice.NewSessions()
	r, w := net.Pipe()
	defer r.Close()
	defer w.Close()
	sess.Add(w, voice.SurfacePCClient)

	s := New(&errSynth{}, sess)
	err := s.Send(context.Background(), delivery.Sendable{Body: "test"})
	if err == nil || !strings.Contains(err.Error(), "synthesize") {
		t.Fatalf("expected synthesize error, got %v", err)
	}
}

type badFormatSynth struct{}

func (b *badFormatSynth) Synthesize(_ context.Context, text string) (audio.Audio, error) {
	return audio.Audio{Format: audio.Format{SampleRate: 999, Channels: 0, SampleBits: 0, Encoding: "bad"}, Bytes: []byte{0, 1, 2, 3}}, nil
}

func TestSinkSend_RejectsBadAudioFormat(t *testing.T) {
	r, w := net.Pipe()
	defer r.Close()
	defer w.Close()
	sess := voice.NewSessions()
	sess.Add(w, voice.SurfacePCClient)

	s := New(&badFormatSynth{}, sess)
	err := s.Send(context.Background(), delivery.Sendable{Body: "test"})
	if err == nil {
		t.Fatal("expected error for bad audio format, got nil")
	}
	if !strings.Contains(err.Error(), "PCM16kMono") {
		t.Errorf("error = %q, want error containing 'PCM16kMono'", err.Error())
	}
}
