package stt

import (
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
	"testing"
	"time"

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

// scripted — a Transcriber that answers with a fixed text, or fails.
type scripted struct {
	text  string
	err   error
	calls atomic.Int32
}

func (s *scripted) Transcribe(_ context.Context, _ audio.Audio) (string, float64, error) {
	s.calls.Add(1)
	if s.err != nil {
		return "", 0, s.err
	}
	return s.text, 0.9, nil
}

func sample() audio.Audio {
	return audio.Audio{Format: audio.PCM16kMono, Bytes: make([]byte, 3200)}
}

// up builds a Pair whose admission answer is already true, without probing.
func up(remote, floor Transcriber) *Pair {
	p := NewPair(remote, floor, "", time.Minute)
	p.up.Store(true)
	return p
}

func TestPairPrefersTheWorkstation(t *testing.T) {
	t.Parallel()
	remote := &scripted{text: "с рабочей станции"}
	floor := &scripted{text: "с homesrv"}
	text, _, err := up(remote, floor).Transcribe(context.Background(), sample())
	if err != nil {
		t.Fatalf("Transcribe: %v", err)
	}
	if text != "с рабочей станции" {
		t.Fatalf("want the remote transcript, got %q", text)
	}
	if floor.calls.Load() != 0 {
		t.Fatalf("floor was called %d times, want 0", floor.calls.Load())
	}
}

// The turn is what matters. A remote that dies mid-session must cost a worse
// transcript and nothing else. This is the V-486 bar.
func TestPairFallsBackWhenRemoteFails(t *testing.T) {
	t.Parallel()
	remote := &scripted{err: errors.New("connection refused")}
	floor := &scripted{text: "с homesrv"}
	p := up(remote, floor)

	text, conf, err := p.Transcribe(context.Background(), sample())
	if err != nil {
		t.Fatalf("a failed remote must not fail the turn: %v", err)
	}
	if text != "с homesrv" {
		t.Fatalf("want the floor transcript, got %q", text)
	}
	if conf != 0.9 {
		t.Fatalf("want the floor confidence, got %v", conf)
	}
	if p.Available() {
		t.Fatal("a failed request must correct the cached admission answer")
	}

	// The next utterance goes straight to the floor rather than into the
	// same hole.
	if _, _, err := p.Transcribe(context.Background(), sample()); err != nil {
		t.Fatalf("second turn: %v", err)
	}
	if remote.calls.Load() != 1 {
		t.Fatalf("remote called %d times, want 1", remote.calls.Load())
	}
}

func TestPairFallsBackWhenRemoteReturnsBlankSuccess(t *testing.T) {
	t.Parallel()
	remote := &scripted{text: "   "}
	floor := &scripted{text: "с homesrv"}
	p := up(remote, floor)

	text, _, err := p.Transcribe(context.Background(), sample())
	if err != nil {
		t.Fatalf("invalid remote success must fall back: %v", err)
	}
	if text != "с homesrv" || floor.calls.Load() != 1 {
		t.Fatalf("got text=%q floor calls=%d, want floor transcript once", text, floor.calls.Load())
	}
	if p.Available() {
		t.Fatal("invalid remote response must correct cached availability")
	}
}

func TestPairWithNoRemoteIsTheFloor(t *testing.T) {
	t.Parallel()
	floor := &scripted{text: "с homesrv"}
	p := NewPair(nil, floor, "", time.Minute)
	p.Start(context.Background()) // no health url, so this is a no-op
	if p.Available() {
		t.Fatal("an unconfigured remote is never available")
	}
	text, _, err := p.Transcribe(context.Background(), sample())
	if err != nil {
		t.Fatalf("Transcribe: %v", err)
	}
	if text != "с homesrv" {
		t.Fatalf("want the floor transcript, got %q", text)
	}
}

func TestPairWithNoFloorRefuses(t *testing.T) {
	t.Parallel()
	_, _, err := NewPair(nil, nil, "", time.Minute).Transcribe(context.Background(), sample())
	if !errors.Is(err, ErrNoFloor) {
		t.Fatalf("want ErrNoFloor, got %v", err)
	}
}

func TestPairProbeReadsHealth(t *testing.T) {
	t.Parallel()
	var ok atomic.Bool
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if !ok.Load() {
			w.WriteHeader(http.StatusServiceUnavailable)
			return
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer srv.Close()

	p := NewPair(&scripted{text: "remote"}, &scripted{text: "floor"}, srv.URL, time.Minute)
	p.probe(context.Background())
	if p.Available() {
		t.Fatal("a 503 means the card is busy, so the workstation is not available")
	}
	ok.Store(true)
	p.probe(context.Background())
	if !p.Available() {
		t.Fatal("a 200 means the workstation will take work")
	}
}

func TestPairStopIsIdempotent(t *testing.T) {
	t.Parallel()
	p := NewPair(nil, &scripted{}, "", time.Minute)
	p.Stop()
	p.Stop()
}
