package main

import (
	"context"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/kami/maven/internal/audio"
	"github.com/kami/maven/internal/capture"
	"github.com/kami/maven/internal/config"
	"github.com/kami/maven/internal/ipc"
	"github.com/kami/maven/internal/media"
)

// silentTranscriber stands in for mavsttd: one fixed phrase per window, so the
// wiring can be tested without whisper.
type silentTranscriber struct{}

func (silentTranscriber) Transcribe(_ context.Context, _ audio.Audio) (string, float64, error) {
	return "решили купить насос", 1.0, nil
}

func testCaptureWiring(t *testing.T) (*captureWiring, *sync.WaitGroup) {
	t.Helper()
	blobs, err := media.Open(t.TempDir(), 0, 0)
	if err != nil {
		t.Fatal(err)
	}
	rec, err := capture.New(blobs, silentTranscriber{}, nil, capture.Config{})
	if err != nil {
		t.Fatal(err)
	}
	var wg sync.WaitGroup
	return &captureWiring{
		rec: rec,
		st:  newTestStore(t),
		cfg: &config.CaptureConfig{},
		now: time.Now,
		ctx: context.Background(),
		wg:  &wg,
	}, &wg
}

// A frame carrying the wrong token must not land in the running session. Append
// and stop used to address "whatever is running now", so a client whose session
// had already ended went on recording into somebody else's meeting, and any
// client could end a recording it never started.
func TestCaptureRefusesAnotherClientsToken(t *testing.T) {
	c, _ := testCaptureWiring(t)
	start, err := c.start(context.Background(), ipc.CaptureStartReq{Label: "встреча"})
	if err != nil {
		t.Fatal(err)
	}
	if start.Token == "" {
		t.Fatal("start handed back no session token")
	}
	if _, err := c.append(context.Background(), ipc.CaptureAppendReq{
		Token: "not-mine",
		Audio: audio.Audio{Format: audio.PCM16kMono, Bytes: make([]byte, 3200)},
	}); err == nil {
		t.Error("a frame with the wrong token was accepted")
	}
	if _, err := c.stop(context.Background(), ipc.CaptureStopReq{Token: "not-mine"}); err == nil {
		t.Error("a stop with the wrong token ended the session")
	}
	if st, _ := c.status(context.Background()); !st.Running {
		t.Error("the session was ended by a client that does not own it")
	}
}

// Stop answers with the transcript and does not wait for the summary. The
// summary is up to forty model calls, and holding the IPC request for them meant
// the client that said "стоп" sat with no answer for minutes.
//
// With no summariser wired the note still has to be written, from the transcript.
// save_transcript is about keeping the verbatim record IN ADDITION to a summary,
// not about whether the meeting is remembered at all — without this fallback a
// dead llama-server meant an hour of meeting left no note behind.
func TestStopReturnsTranscriptAndNotesItWithoutASummary(t *testing.T) {
	c, wg := testCaptureWiring(t)
	start, err := c.start(context.Background(), ipc.CaptureStartReq{Label: "планёрка"})
	if err != nil {
		t.Fatal(err)
	}
	if _, err := c.append(context.Background(), ipc.CaptureAppendReq{
		Token: start.Token,
		Audio: audio.Audio{Format: audio.PCM16kMono, Bytes: make([]byte, 32000)},
	}); err != nil {
		t.Fatal(err)
	}
	resp, err := c.stop(context.Background(), ipc.CaptureStopReq{Token: start.Token})
	if err != nil {
		t.Fatalf("stop: %v", err)
	}
	if resp.Transcript == "" {
		t.Fatal("stop returned no transcript")
	}
	if resp.Summary != "" {
		t.Errorf("summary = %q, want none inside the request", resp.Summary)
	}
	wg.Wait()

	notes, err := c.st.RecentNotes(context.Background(), 10)
	if err != nil {
		t.Fatal(err)
	}
	var found bool
	for _, n := range notes {
		if strings.Contains(n.Text, "насос") {
			found = true
		}
	}
	if !found {
		t.Fatalf("the meeting left no note behind: %+v", notes)
	}
}

// The summary path is JSON-wrapped by summaryGrammar, and internal/capture must
// keep seeing plain prose. These cover the wrapper and every way it can be
// absent or broken, because a meeting summary is written once and not retried.
func TestUnwrapSummary(t *testing.T) {
	cases := []struct {
		name string
		in   string
		want string
	}{
		{"grammar output", `{"summary": "решили купить насос"}`, "решили купить насос"},
		{"multiline field", `{"summary": "- насос\n- бюджет"}`, "- насос\n- бюджет"},
		{"empty marker survives", `{"summary": "пусто"}`, "пусто"},
		{"empty field says nothing", `{"summary": ""}`, ""},
		{"thinking prefix", "<think>hm</think>\n{\"summary\": \"итог\"}", "итог"},
		{"no grammar, plain prose", "решили купить насос", "решили купить насос"},
		{"broken json falls back", `{"summary": "обрыв`, `{"summary": "обрыв`},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if got := unwrapSummary(c.in); got != c.want {
				t.Errorf("unwrapSummary(%q) = %q, want %q", c.in, got, c.want)
			}
		})
	}
}
