package capture

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

// A frame for a session that already ended must not land in the next one. The
// recorder used to be addressed as "whatever is running now", so a client whose
// session was reaped went on appending its microphone into a meeting somebody
// else had started.
func TestAppendWithTheWrongTokenIsRefused(t *testing.T) {
	r, _ := testRecorder(t, &fakeTranscriber{}, nil, Config{})
	s, err := r.Start("первая")
	if err != nil {
		t.Fatal(err)
	}
	if err := r.Append("someone-elses-token", frame(1)); !errors.Is(err, ErrWrongSession) {
		t.Fatalf("append = %v, want ErrWrongSession", err)
	}
	if _, err := r.Stop(context.Background(), "someone-elses-token"); !errors.Is(err, ErrWrongSession) {
		t.Fatalf("stop = %v, want ErrWrongSession", err)
	}
	if r.Abort("someone-elses-token") {
		t.Fatal("Abort discarded a session it does not own")
	}
	if err := r.Append(s.Token, frame(1)); err != nil {
		t.Fatalf("the owner is still refused: %v", err)
	}
}

// A client that simply stops sending — a phone whose tab was closed — used to
// hold the single session slot forever, and every later Start answered ErrBusy
// with a meeting from last week.
func TestStaleSessionIsReapedByTheWallClock(t *testing.T) {
	r, _ := testRecorder(t, &fakeTranscriber{}, nil, Config{MaxDuration: time.Minute})
	now := time.Now().UTC()
	r.now = func() time.Time { return now }
	s, err := r.Start("брошенная")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := r.Start("вторая"); !errors.Is(err, ErrBusy) {
		t.Fatalf("start = %v, want ErrBusy", err)
	}
	now = now.Add(time.Minute + StaleGrace + time.Second)
	next, err := r.Start("вторая")
	if err != nil {
		t.Fatalf("a stale session was not reaped: %v", err)
	}
	if next.Token == s.Token {
		t.Fatal("the new session reused the stale token")
	}
	if err := r.Append(s.Token, frame(1)); !errors.Is(err, ErrWrongSession) {
		t.Fatalf("the reaped client can still write: %v", err)
	}
	// The abandoned recording is not kept: nobody is waiting for it, and storing
	// it would mean keeping a meeting on the strength of a dropped connection.
	if _, err := os.Stat(s.path); !os.IsNotExist(err) {
		t.Fatalf("the reaped spool file survived: %v", err)
	}
}

// One window failing used to fail the whole transcription, which threw away
// every other window of an hour-long meeting. The hole is marked instead, so the
// summary cannot silently read as if nothing was missing.
func TestOneFailedWindowIsMarkedNotFatal(t *testing.T) {
	r, _ := testRecorder(t, &fakeTranscriber{}, nil, Config{STTWindow: time.Second})
	r.tr = &windowTranscriber{failOn: 2}
	s, err := r.Start("встреча")
	if err != nil {
		t.Fatal(err)
	}
	if err := r.Append(s.Token, frame(3)); err != nil {
		t.Fatal(err)
	}
	res, err := r.Stop(context.Background(), s.Token)
	if err != nil {
		t.Fatalf("stop: %v", err)
	}
	if !strings.Contains(res.Transcript, gapMarker) {
		t.Errorf("no gap marker in %q", res.Transcript)
	}
	if !strings.Contains(res.Transcript, "окно1") || !strings.Contains(res.Transcript, "окно3") {
		t.Errorf("the surviving windows were dropped: %q", res.Transcript)
	}
}

// The audio not fitting the store is not a reason to lose the words. Stop used
// to return early on a store failure, so a recording over the blob cap produced
// neither a blob nor a transcript.
func TestStoreFailureStillTranscribes(t *testing.T) {
	blobs, err := media.OpenWithBudget(t.TempDir(), 512, 1024, 0)
	if err != nil {
		t.Fatal(err)
	}
	tr := &fakeTranscriber{}
	r, err := New(blobs, tr, nil, Config{})
	if err != nil {
		t.Fatal(err)
	}
	s, err := r.Start("длинная встреча")
	if err != nil {
		t.Fatal(err)
	}
	if err := r.Append(s.Token, frame(2)); err != nil {
		t.Fatal(err)
	}
	// The store failure is reported, but as a degraded success: the Result is
	// filled in, and the caller keeps it rather than treating the error as
	// nothing having happened.
	res, err := r.Stop(context.Background(), s.Token)
	if err == nil {
		t.Fatal("the store failure was not reported")
	}
	if res.BlobID != "" {
		t.Errorf("blob id = %q, want none", res.BlobID)
	}
	if !errors.Is(res.StoreErr, media.ErrTooLarge) {
		t.Errorf("StoreErr = %v, want ErrTooLarge", res.StoreErr)
	}
	if res.Transcript == "" {
		t.Fatal("the words were lost with the audio")
	}
	// The spool file is cleaned up even on the failure path.
	glob, _ := filepath.Glob(filepath.Join(blobs.Dir(), "spool", "*"))
	if len(glob) != 0 {
		t.Errorf("spool leaked: %v", glob)
	}
}

// windowTranscriber answers per window and fails a chosen one, which is what a
// whisper timeout in the middle of a meeting looks like.
type windowTranscriber struct {
	calls  int
	failOn int
}

func (w *windowTranscriber) Transcribe(_ context.Context, _ audio.Audio) (string, float64, error) {
	w.calls++
	if w.calls == w.failOn {
		return "", 0, errors.New("whisper timed out")
	}
	return fmt.Sprintf("окно%d", w.calls), 1.0, nil
}
