package voice

import (
	"context"
	"encoding/json"
	"net"
	"os"
	"syscall"
	"testing"
	"time"

	"github.com/kami/maven/internal/audio"
)

// newTestListener returns a TCP listener on a random port with SO_REUSEADDR.
// It retries a few times if the port is temporarily unavailable (e.g., TIME_WAIT).
func newTestListener(t *testing.T) net.Listener {
	t.Helper()
	lc := net.ListenConfig{
		Control: func(network, address string, c syscall.RawConn) error {
			var err error
			c.Control(func(fd uintptr) {
				err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
			})
			return err
		},
	}
	var ln net.Listener
	var err error
	for i := 0; i < 5; i++ {
		ln, err = lc.Listen(context.Background(), "tcp", "127.0.0.1:0")
		if err == nil {
			break
		}
		if !isAddrInUse(err) {
			t.Fatalf("listen: %v", err)
		}
		time.Sleep(100 * time.Millisecond)
	}
	if err != nil {
		t.Fatalf("listen after retries: %v", err)
	}
	// Close the probe before returning: it only existed to reserve a free port
	// (127.0.0.1:0 → a concrete port). The Server rebinds that exact addr in its
	// own Listen(), which fails with EADDRINUSE while the probe still holds it.
	// Addr() keeps returning the address after Close, and an unconnected listener
	// leaves no TIME_WAIT, so the rebind is immediate and clean.
	_ = ln.Close()
	return ln
}

func isAddrInUse(err error) bool {
	if err == nil {
		return false
	}
	if opErr, ok := err.(*net.OpError); ok {
		if sysErr, ok := opErr.Err.(*os.SyscallError); ok {
			if err := sysErr.Err; err == syscall.EADDRINUSE {
				return true
			}
		}
	}
	return false
}

// wait a bit for OS to release the port after close.
func waitPort() { time.Sleep(500 * time.Millisecond) }

// stubHandler — satisfies voice.Handler for tests.
type stubHandler struct {
	lastReq PushToTalkReq
}

func (h *stubHandler) HandlePushToTalk(_ context.Context, req PushToTalkReq, _ uint64) (PushToTalkResp, error) {
	h.lastReq = req
	return PushToTalkResp{
		ReplyAudio: audio.Audio{Format: audio.PCM16kMono, Bytes: []byte("reply")},
		ReplyText:  "got it",
	}, nil
}

func TestServerAcceptsAndRemovesSession(t *testing.T) {
	l := newTestListener(t)
	sess := NewSessions()
	h := &stubHandler{}
	srv := NewServer(l.Addr().String(), h, sess)
	if err := srv.Listen(); err != nil {
		t.Fatalf("listen: %v", err)
	}
	defer func() {
		_ = srv.Close()
		waitPort()
	}()
	// Accept connections in the background; Serve blocks until Close. Without
	// it the listener binds but never accepts, so a client round-trip hangs.
	go func() { _ = srv.Serve() }()

	if sess.Active() != 0 {
		t.Fatalf("active before connect: %d", sess.Active())
	}

	conn, err := net.Dial("tcp", l.Addr().String())
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	conn.Close() // quick disconnect

	// Give serveConn a moment to register then remove.
	time.Sleep(50 * time.Millisecond)
	if sess.Active() != 0 {
		t.Fatalf("active after disconnect: %d, want 0", sess.Active())
	}
}

func TestClientPushToTalkRoundTrip(t *testing.T) {
	l := newTestListener(t)
	sess := NewSessions()
	h := &stubHandler{}
	srv := NewServer(l.Addr().String(), h, sess)
	if err := srv.Listen(); err != nil {
		t.Fatalf("listen: %v", err)
	}
	defer func() {
		_ = srv.Close()
		waitPort()
	}()
	// Accept connections in the background; Serve blocks until Close. Without
	// it the listener binds but never accepts, so a client round-trip hangs.
	go func() { _ = srv.Serve() }()

	c := Dial(l.Addr().String())
	defer c.Close()

	resp, err := c.PushToTalk(context.Background(), audio.Audio{Format: audio.PCM16kMono, Bytes: []byte("hello")}, "ru")
	if err != nil {
		t.Fatalf("PushToTalk: %v", err)
	}
	if resp.ReplyText != "got it" {
		t.Fatalf("ReplyText: %q, want %q", resp.ReplyText, "got it")
	}
	if len(resp.ReplyAudio.Bytes) == 0 {
		t.Fatalf("ReplyAudio empty")
	}
	if h.lastReq.Audio.Bytes == nil {
		t.Fatalf("handler never got audio")
	}
}

func TestClientListenModeReceivesPush(t *testing.T) {
	l := newTestListener(t)
	sess := NewSessions()
	h := &stubHandler{}
	srv := NewServer(l.Addr().String(), h, sess)
	if err := srv.Listen(); err != nil {
		t.Fatalf("listen: %v", err)
	}
	defer func() {
		_ = srv.Close()
		waitPort()
	}()
	// Accept connections in the background; Serve blocks until Close. Without
	// it the listener binds but never accepts, so a client round-trip hangs.
	go func() { _ = srv.Serve() }()

	c := Dial(l.Addr().String())
	defer c.Close()

	// Run the push receiver in the background: it blocks reading frames until
	// the conn closes (ctx cancel alone can't interrupt a blocking read). A
	// proactive push from the server is delivered to the handler, which hands
	// the audio to a channel the test waits on.
	got := make(chan audio.Audio, 1)
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go func() {
		_ = c.RunPushReceiver(ctx, pushHandlerFunc(func(p Push) {
			if p.Kind != PushKindAudioNudge {
				return
			}
			var ap AudioNudgePush
			if err := json.Unmarshal(p.Params, &ap); err == nil {
				select {
				case got <- ap.Audio:
				default:
				}
			}
		}))
	}()

	// serveConn registers the session on accept, but Accept→Add is async — wait
	// for it before pushing, or PushToMostRecent finds no session.
	for i := 0; i < 100 && sess.Active() < 1; i++ {
		time.Sleep(10 * time.Millisecond)
	}
	if sess.Active() < 1 {
		t.Fatal("session never registered")
	}

	// Server pushes to the session.
	push := AudioNudgePush{
		RuleName: "test",
		Severity: 3,
		Audio:    audio.Audio{Format: audio.PCM16kMono, Bytes: []byte("proactive")},
		Text:     "proactive text",
		Ts:       time.Now(),
	}
	if err := sess.PushToMostRecent(context.Background(), push); err != nil {
		t.Fatalf("PushToMostRecent: %v", err)
	}

	select {
	case received := <-got:
		if string(received.Bytes) != "proactive" {
			t.Fatalf("received audio mismatch: %q", string(received.Bytes))
		}
	case <-time.After(2 * time.Second):
		t.Fatal("never received pushed audio")
	}
}

type pushHandlerFunc func(Push)

func (f pushHandlerFunc) OnPush(p Push) { f(p) }
