package backend

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"image"
	"image/color"
	"image/jpeg"
	"io"
	"math"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"

	"github.com/mem9-ai/drive9/pkg/datastore"
	"github.com/mem9-ai/drive9/pkg/semantic"
)

func testVideoJPEG(t *testing.T, width, height int) []byte {
	t.Helper()
	var out bytes.Buffer
	if err := jpeg.Encode(&out, image.NewRGBA(image.Rect(0, 0, width, height)), nil); err != nil {
		t.Fatal(err)
	}
	return out.Bytes()
}

func testVideoPatternJPEG(t *testing.T, seed int) []byte {
	t.Helper()
	img := image.NewGray(image.Rect(0, 0, 90, 80))
	for y := 0; y < 80; y++ {
		for x := 0; x < 90; x++ {
			img.SetGray(x, y, color.Gray{Y: uint8((x*17 + y*29 + seed*37 + (x*y*seed)%251) % 256)})
		}
	}
	var out bytes.Buffer
	if err := jpeg.Encode(&out, img, &jpeg.Options{Quality: 95}); err != nil {
		t.Fatal(err)
	}
	return out.Bytes()
}

type staticVideoExtractor struct {
	text  string
	usage VideoExtractUsage
	err   error
	calls int
}

type multiCallVideoExtractor struct{}

func (multiCallVideoExtractor) ExtractVideoText(_ context.Context, req VideoExtractRequest) (string, VideoExtractUsage, error) {
	if req.BeforeProviderCall != nil {
		req.BeforeProviderCall()
		req.BeforeProviderCall()
	}
	return "visual", VideoExtractUsage{}, nil
}

type scriptedSegmentedVideoExtractor struct {
	duration       float64
	failSegment    int
	callDelay      time.Duration
	segmentOps     int
	synthesisText  string
	segmentCalls   []int
	synthesisCalls int
}

func (e *scriptedSegmentedVideoExtractor) ExtractVideoText(context.Context, VideoExtractRequest) (string, VideoExtractUsage, error) {
	return "", VideoExtractUsage{}, errors.New("legacy whole-video extraction must not run")
}

func (e *scriptedSegmentedVideoExtractor) ProbeVideoDuration(context.Context, VideoExtractRequest) (float64, error) {
	return e.duration, nil
}

func (e *scriptedSegmentedVideoExtractor) ExtractVideoSegment(ctx context.Context, req VideoExtractRequest, segment VideoVisualSegment) (string, VideoExtractUsage, error) {
	e.segmentCalls = append(e.segmentCalls, segment.Index)
	if segment.Index == e.failSegment {
		e.failSegment = -1
		return "", VideoExtractUsage{}, errors.New("temporary segment failure")
	}
	if req.BeforeProviderCall != nil {
		req.BeforeProviderCall()
	}
	operations := max(1, e.segmentOps)
	for range operations {
		if e.callDelay > 0 {
			select {
			case <-time.After(e.callDelay):
			case <-ctx.Done():
				return "", VideoExtractUsage{}, ctx.Err()
			}
		}
	}
	return fmt.Sprintf(`{"caption_en":"segment %d","attributes":{"scene_%d":"present"}}`, segment.Index, segment.Index), VideoExtractUsage{PromptTokens: 10, CompletionTokens: 5, FramesExtracted: 8}, nil
}

func (e *scriptedSegmentedVideoExtractor) SynthesizeVideoText(ctx context.Context, req VideoExtractRequest, summaries string) (string, VideoExtractUsage, error) {
	e.synthesisCalls++
	if req.BeforeProviderCall != nil {
		req.BeforeProviderCall()
	}
	if e.callDelay > 0 {
		select {
		case <-time.After(e.callDelay):
		case <-ctx.Done():
			return "", VideoExtractUsage{}, ctx.Err()
		}
	}
	if e.synthesisText != "" {
		return e.synthesisText, VideoExtractUsage{PromptTokens: 20, CompletionTokens: 10}, nil
	}
	return fmt.Sprintf(`{"caption_en":"final from %d segments","description_en":%q}`, len(e.segmentCalls), summaries), VideoExtractUsage{PromptTokens: 20, CompletionTokens: 10}, nil
}

func TestOpenAIVideoDescribeFramesDoesNotInventTimestamps(t *testing.T) {
	var requestBody string
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		if err != nil {
			t.Errorf("read request: %v", err)
		}
		requestBody = string(body)
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"timeline summary"}}]}`))
	}))
	defer ts.Close()

	extractor, err := NewOpenAIVideoTextExtractor(OpenAIVideoTextExtractorConfig{
		BaseURL: ts.URL, APIKey: "test-key", Model: "vision-model", Client: ts.Client(),
	})
	if err != nil {
		t.Fatal(err)
	}
	text, _, err := extractor.describeFramesInBatches(context.Background(), [][]byte{{1, 2, 3}, {4, 5, 6}}, nil)
	if err != nil {
		t.Fatal(err)
	}
	if text != "timeline summary" {
		t.Fatalf("text = %q, want timeline summary", text)
	}
	if strings.Contains(requestBody, "Frame timestamp") {
		t.Fatalf("vision request contains fabricated timestamp: %s", requestBody)
	}
	if got := strings.Count(requestBody, `"type":"image_url"`); got != 2 {
		t.Fatalf("image_url count = %d, want 2; body=%s", got, requestBody)
	}
}

func TestOpenAIVideoSynthesisUsesOrderedTextWithoutResendingImages(t *testing.T) {
	var requestBody string
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		if err != nil {
			t.Errorf("read request: %v", err)
		}
		requestBody = string(body)
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"final timeline summary"}}]}`))
	}))
	defer ts.Close()

	extractor, err := NewOpenAIVideoTextExtractor(OpenAIVideoTextExtractorConfig{
		BaseURL: ts.URL, APIKey: "test-key", Model: "vision-model", Client: ts.Client(),
	})
	if err != nil {
		t.Fatal(err)
	}
	summaries := "[Segment 1 0-300s]\nopening\n[Segment 2 300-600s]\nending"
	text, _, err := extractor.SynthesizeVideoText(context.Background(), VideoExtractRequest{}, summaries)
	if err != nil {
		t.Fatal(err)
	}
	if text != "final timeline summary" || !strings.Contains(requestBody, "opening") || !strings.Contains(requestBody, "ending") {
		t.Fatalf("text=%q body=%s, want ordered segment synthesis", text, requestBody)
	}
	if strings.Contains(requestBody, `"type":"image_url"`) {
		t.Fatalf("synthesis resent images: %s", requestBody)
	}
}

func TestValidateVideoFrameRejectsDimensionLimit(t *testing.T) {
	data := testVideoJPEG(t, maxVideoFrameDimension+1, 1)
	if err := validateVideoFrame(data); !errors.Is(err, ErrVideoFrameTooLarge) {
		t.Fatalf("validateVideoFrame error=%v, want ErrVideoFrameTooLarge", err)
	}
}

func TestValidateVideoFrameAcceptsSquareFrameProducedByScaleFilter(t *testing.T) {
	data := testVideoJPEG(t, maxVideoFrameDimension, maxVideoFrameDimension)
	if err := validateVideoFrame(data); err != nil {
		t.Fatalf("validateVideoFrame error=%v, want square scaled frame accepted", err)
	}
}

func TestVideoFrameFilterCapsDimensions(t *testing.T) {
	filter := videoFrameFilter(12)
	if !strings.Contains(filter, "scale=") || !strings.Contains(filter, fmt.Sprint(maxVideoFrameDimension)) {
		t.Fatalf("filter=%q, want bounded scale", filter)
	}
}

func TestVideoSampleTimestampsUseConfiguredCadence(t *testing.T) {
	timestamps := videoSampleTimestamps(100, 5)
	if len(timestamps) != 21 {
		t.Fatalf("timestamps=%v, want 20 cadence samples plus near-end boundary", timestamps)
	}
	if timestamps[0] != 0 {
		t.Fatalf("first timestamp=%v, want 0", timestamps[0])
	}
	if timestamps[len(timestamps)-1] != 99.9 {
		t.Fatalf("last timestamp=%v, want 99.9 near-end boundary", timestamps[len(timestamps)-1])
	}
}

func TestLegacyVideoSampleTimestampsStayBoundedAcrossTimeline(t *testing.T) {
	timestamps := videoSampleTimestamps(2*60*60, 5, 10)
	if len(timestamps) != 10 {
		t.Fatalf("timestamps=%v, want 10 bounded samples", timestamps)
	}
	if timestamps[0] != 0 || timestamps[len(timestamps)-1] != 2*60*60-0.1 {
		t.Fatalf("timestamps=%v, want complete timeline boundaries", timestamps)
	}
}

func TestLegacyVideoFrameExtractionCapsFFmpegCalls(t *testing.T) {
	binDir := t.TempDir()
	logPath := filepath.Join(t.TempDir(), "ffmpeg-calls")
	ffmpeg := filepath.Join(binDir, "ffmpeg")
	script := fmt.Sprintf("#!/bin/sh\nprintf x >> %q\nout=''\nfor arg in \"$@\"; do out=\"$arg\"; done\n: > \"$out\"\n", logPath)
	if err := os.WriteFile(ffmpeg, []byte(script), 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(binDir, "ffprobe"), []byte("#!/bin/sh\nprintf '100\\n'\n"), 0o700); err != nil {
		t.Fatal(err)
	}
	sourcePath := filepath.Join(t.TempDir(), "source.mp4")
	if err := os.WriteFile(sourcePath, []byte("video"), 0o600); err != nil {
		t.Fatal(err)
	}
	extractor := &OpenAIVideoTextExtractor{ffmpegPath: ffmpeg, frameInterval: 5}
	if _, _, err := extractor.extractFrames(context.Background(), nil, "video/mp4", sourcePath); err != nil {
		t.Fatal(err)
	}
	calls, err := os.ReadFile(logPath)
	if err != nil {
		t.Fatal(err)
	}
	if len(calls) != defaultLegacyVideoMaxFrames {
		t.Fatalf("ffmpeg calls=%d, want %d bounded timeline samples", len(calls), defaultLegacyVideoMaxFrames)
	}
}

func TestLegacyVideoFrameExtractionFailsClosedWithoutDuration(t *testing.T) {
	binDir := t.TempDir()
	marker := filepath.Join(t.TempDir(), "ffmpeg-called")
	ffmpeg := filepath.Join(binDir, "ffmpeg")
	if err := os.WriteFile(ffmpeg, []byte(fmt.Sprintf("#!/bin/sh\n: > %q\n", marker)), 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(binDir, "ffprobe"), []byte("#!/bin/sh\nexit 1\n"), 0o700); err != nil {
		t.Fatal(err)
	}
	sourcePath := filepath.Join(t.TempDir(), "source.mp4")
	if err := os.WriteFile(sourcePath, []byte("video"), 0o600); err != nil {
		t.Fatal(err)
	}
	extractor := &OpenAIVideoTextExtractor{ffmpegPath: ffmpeg, frameInterval: 5}
	if _, _, err := extractor.extractFrames(context.Background(), nil, "video/mp4", sourcePath); err == nil {
		t.Fatal("extractFrames succeeded without a duration bound")
	}
	if _, err := os.Stat(marker); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("ffmpeg marker error=%v, want no frame extraction", err)
	}
}

func TestVideoVisualSegmentsCoverTwoHourTimeline(t *testing.T) {
	segments, err := videoVisualSegments(2 * 60 * 60)
	if err != nil {
		t.Fatal(err)
	}
	if len(segments) != 24 {
		t.Fatalf("segments=%d, want 24 five-minute segments", len(segments))
	}
	if first := segments[0]; first.Index != 0 || first.StartSeconds != 0 || first.EndSeconds != 300 {
		t.Fatalf("first segment=%+v, want [0,300]", first)
	}
	if last := segments[len(segments)-1]; last.Index != 23 || last.StartSeconds != 6900 || last.EndSeconds != 7200 {
		t.Fatalf("last segment=%+v, want [6900,7200]", last)
	}
}

func TestVideoVisualSegmentsRejectDurationBeyondSupportedTimeline(t *testing.T) {
	for _, duration := range []float64{2*60*60 + 0.001, math.MaxFloat64} {
		if _, err := videoVisualSegments(duration); !errors.Is(err, ErrVideoDurationTooLong) {
			t.Fatalf("videoVisualSegments(%v) error=%v, want ErrVideoDurationTooLong", duration, err)
		}
	}
}

func TestVideoVisualSegmentsRejectNonFiniteDuration(t *testing.T) {
	for _, duration := range []float64{math.NaN(), math.Inf(1), math.Inf(-1)} {
		if _, err := videoVisualSegments(duration); err == nil {
			t.Fatalf("videoVisualSegments(%v) error=nil, want invalid duration", duration)
		}
	}
}

func TestProbeVideoDurationReturnsContextDeadline(t *testing.T) {
	binDir := t.TempDir()
	ffmpegPath := filepath.Join(binDir, "ffmpeg")
	if err := os.WriteFile(ffmpegPath, []byte("#!/bin/sh\nexit 0\n"), 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(binDir, "ffprobe"), []byte("#!/bin/sh\nsleep 10\n"), 0o700); err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()
	if _, err := probeVideoDuration(ctx, ffmpegPath, "/video.mp4"); !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("probeVideoDuration error=%v, want context deadline exceeded", err)
	}
}

func TestVideoSegmentSampleTimestampsUseFiveSecondCadence(t *testing.T) {
	timestamps := videoSegmentSampleTimestamps(VideoVisualSegment{StartSeconds: 300, EndSeconds: 600}, 5)
	if len(timestamps) != 61 {
		t.Fatalf("timestamps=%v, want 60 cadence samples plus near-end boundary", timestamps)
	}
	if timestamps[0] != 300 || timestamps[len(timestamps)-1] != 599.9 {
		t.Fatalf("timestamps=%v, want five-second cadence and near-end boundary", timestamps)
	}
}

func TestOpenAIVideoSegmentUsesCadenceAndNearEndFFmpegSamples(t *testing.T) {
	binDir := t.TempDir()
	ffmpeg := filepath.Join(binDir, "ffmpeg")
	logPath := filepath.Join(binDir, "ffmpeg.log")
	framePath := filepath.Join(binDir, "frame.jpg")
	if err := os.WriteFile(framePath, testVideoJPEG(t, 64, 64), 0o600); err != nil {
		t.Fatal(err)
	}
	script := `#!/bin/sh
printf '%s\n' "$*" >> "$FAKE_VIDEO_FFMPEG_LOG"
out=""
for arg in "$@"; do out="$arg"; done
case "$out" in
  *%04d*)
    i=1
	    while [ "$i" -le 60 ]; do
      suffix=$(printf '%04d' "$i")
      path=$(printf '%s' "$out" | sed "s/%04d/$suffix/")
      cp "$FAKE_VIDEO_FRAME" "$path"
      i=$((i + 1))
    done
    ;;
  *) cp "$FAKE_VIDEO_FRAME" "$out" ;;
esac
`
	if err := os.WriteFile(ffmpeg, []byte(script), 0o700); err != nil {
		t.Fatal(err)
	}
	sourcePath := filepath.Join(binDir, "source.mp4")
	if err := os.WriteFile(sourcePath, []byte("video"), 0o600); err != nil {
		t.Fatal(err)
	}
	t.Setenv("FAKE_VIDEO_FFMPEG_LOG", logPath)
	t.Setenv("FAKE_VIDEO_FRAME", framePath)

	extractor, err := NewOpenAIVideoTextExtractor(OpenAIVideoTextExtractorConfig{
		BaseURL: "https://api.example.com", APIKey: "key", Model: "vision", FFmpegPath: ffmpeg,
	})
	if err != nil {
		t.Fatal(err)
	}
	frames, retained, err := extractor.extractFramesForSegment(context.Background(), nil, "video/mp4", sourcePath, VideoVisualSegment{
		Index: 1, StartSeconds: 300, EndSeconds: 600,
	})
	if err != nil {
		t.Fatal(err)
	}
	if len(frames) != 1 || retained != 1 {
		t.Fatalf("provider_frames=%d retained=%d, want duplicate samples collapsed to one", len(frames), retained)
	}
	logData, err := os.ReadFile(logPath)
	if err != nil {
		t.Fatal(err)
	}
	lines := strings.Split(strings.TrimSpace(string(logData)), "\n")
	if len(lines) != 2 {
		t.Fatalf("ffmpeg invocations=%d, want cadence plus near-end sample; args=%q", len(lines), lines)
	}
	for _, want := range []string{"-ss 300.000", "-t 300.000", "fps=1/5"} {
		if !strings.Contains(lines[0], want) {
			t.Fatalf("ffmpeg args=%q, want %q", lines[0], want)
		}
	}
	if strings.Contains(lines[0], "-frames:v") {
		t.Fatalf("ffmpeg args=%q must not impose a fixed frame count", lines[0])
	}
	for _, want := range []string{"-ss 599.900", "-frames:v 1"} {
		if !strings.Contains(lines[1], want) {
			t.Fatalf("near-end ffmpeg args=%q, want %q", lines[1], want)
		}
	}
}

func TestLoadDeduplicatedVideoProviderFramesIncludesAllDistinctFrames(t *testing.T) {
	dir := t.TempDir()
	paths := make([]string, 12)
	for i := range paths {
		paths[i] = filepath.Join(dir, fmt.Sprintf("frame_%04d.jpg", i+1))
		if err := os.WriteFile(paths[i], testVideoPatternJPEG(t, i+1), 0o600); err != nil {
			t.Fatal(err)
		}
	}
	providerFrames, retained := loadDeduplicatedVideoProviderFrames(paths)
	if retained != 12 || len(providerFrames) != 3 {
		t.Fatalf("retained=%d provider_frames=%d, want 12 frames in 3 sheets", retained, len(providerFrames))
	}
}

func TestOpenAIVideoFrameBatchesRespectPayloadLimitWithoutDroppingFrames(t *testing.T) {
	const maxPayloadBytes = int64(3500)
	var requestBodies [][]byte
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		if err != nil {
			t.Errorf("read request: %v", err)
		}
		requestBodies = append(requestBodies, body)
		content := "batch summary"
		if !bytes.Contains(body, []byte(`"type":"image_url"`)) {
			content = "combined segment summary"
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = fmt.Fprintf(w, `{"choices":[{"message":{"content":%q}}],"usage":{"prompt_tokens":2,"completion_tokens":3}}`, content)
	}))
	defer ts.Close()

	extractor, err := NewOpenAIVideoTextExtractor(OpenAIVideoTextExtractorConfig{
		BaseURL: ts.URL, APIKey: "test-key", Model: "vision", Prompt: "describe", Client: ts.Client(),
		MaxProviderPayloadBytes: maxPayloadBytes,
	})
	if err != nil {
		t.Fatal(err)
	}
	frames := [][]byte{make([]byte, 1500), make([]byte, 1500), make([]byte, 1500)}
	text, usage, err := extractor.describeFramesInBatches(context.Background(), frames, nil)
	if err != nil {
		t.Fatal(err)
	}
	if text != "combined segment summary" {
		t.Fatalf("text=%q, want combined segment summary", text)
	}
	imageCount := 0
	imageRequests := 0
	for _, body := range requestBodies {
		if int64(len(body)) > maxPayloadBytes {
			t.Fatalf("request body bytes=%d, want <= %d", len(body), maxPayloadBytes)
		}
		count := bytes.Count(body, []byte(`"type":"image_url"`))
		imageCount += count
		if count > 0 {
			imageRequests++
		}
	}
	if imageCount != len(frames) || imageRequests <= 1 {
		t.Fatalf("image_count=%d image_requests=%d, want all %d frames across multiple batches", imageCount, imageRequests, len(frames))
	}
	if usage.PromptTokens != 8 || usage.CompletionTokens != 12 || usage.FramesExtracted != len(frames) {
		t.Fatalf("usage=%+v, want aggregate usage across three batches and synthesis", usage)
	}
}

func TestOpenAIVideoFrameBatchesPreserveSuccessfulUsageBeforeFailure(t *testing.T) {
	const maxPayloadBytes = int64(3500)
	requests := 0
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		requests++
		if requests == 2 {
			http.Error(w, "temporary failure", http.StatusServiceUnavailable)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = io.WriteString(w, `{"choices":[{"message":{"content":"first batch"}}],"usage":{"prompt_tokens":7,"completion_tokens":11}}`)
	}))
	defer ts.Close()

	extractor, err := NewOpenAIVideoTextExtractor(OpenAIVideoTextExtractorConfig{
		BaseURL: ts.URL, APIKey: "test-key", Model: "vision", Prompt: "describe", Client: ts.Client(),
		MaxProviderPayloadBytes: maxPayloadBytes,
	})
	if err != nil {
		t.Fatal(err)
	}
	_, usage, err := extractor.describeFramesInBatches(context.Background(), [][]byte{
		make([]byte, 1500), make([]byte, 1500),
	}, nil)
	if err == nil {
		t.Fatal("describeFramesInBatches succeeded, want second-batch failure")
	}
	if requests != 2 {
		t.Fatalf("requests=%d, want 2", requests)
	}
	if usage.PromptTokens != 7 || usage.CompletionTokens != 11 {
		t.Fatalf("usage=%+v, want first successful batch usage", usage)
	}
}

func TestVideoCheckpointTextBudgetsFitSemanticTaskPayload(t *testing.T) {
	visual := boundCheckpointText(strings.Repeat("\x00", videoCheckpointTextMaxJSONBytes), videoCheckpointTextMaxJSONBytes)
	audio := boundCheckpointText(strings.Repeat("\x00", videoCheckpointTextMaxJSONBytes), videoCheckpointTextMaxJSONBytes)
	payload := semantic.VideoExtractTaskPayload{
		Path: "/" + strings.Repeat("\\", 4096), ContentType: "video/mp4",
		VisualDone: true, VisualText: visual, AudioDone: true, AudioPresent: true,
		AudioCompletedSegments: maxAudioExtractSegments, AudioText: audio,
	}
	encoded, err := json.Marshal(payload)
	if err != nil {
		t.Fatal(err)
	}
	if len(encoded) > datastore.MaxSemanticTaskPayloadBytes {
		t.Fatalf("video checkpoint payload bytes=%d, want <= %d", len(encoded), datastore.MaxSemanticTaskPayloadBytes)
	}

	tags := make(map[string]string, maxImageExtractAttributes)
	for i := 0; i < maxImageExtractAttributes; i++ {
		tags[fmt.Sprintf("attribute_%02d_%s", i, strings.Repeat("k", 40))] = strings.Repeat("v", 255)
	}
	inProgress := semantic.VideoExtractTaskPayload{
		Path: "/" + strings.Repeat("p", 4096), ContentType: "video/mp4",
		VisualCompletedSegments: defaultVideoVisualMaxSegments,
		VisualSegmentCount:      defaultVideoVisualMaxSegments,
		VisualSegmentsText:      strings.Repeat("s", videoVisualSegmentsMaxJSONBytes),
		AudioCompletedSegments:  maxAudioExtractSegments,
		AudioText:               audio,
		VisualTags:              tags,
	}
	encoded, err = json.Marshal(inProgress)
	if err != nil {
		t.Fatal(err)
	}
	if len(encoded) > datastore.MaxSemanticTaskPayloadBytes {
		t.Fatalf("in-progress video checkpoint payload bytes=%d, want <= %d", len(encoded), datastore.MaxSemanticTaskPayloadBytes)
	}
}

func TestCheckpointVideoExtractDoesNotFailWhenFullPayloadIsTooLarge(t *testing.T) {
	err := (&Dat9Backend{}).checkpointVideoExtract(func(VideoExtractCheckpoint) error {
		return datastore.ErrSemanticTaskPayloadTooLarge
	}, VideoExtractCheckpoint{VisualDone: true})
	if err != nil {
		t.Fatalf("checkpoint error=%v, want oversized payload to keep the previous cursor", err)
	}
}

func TestProcessVideoExtractTaskRetriesFromLastDurableVisualSegment(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/rejected-checkpoint.mp4", "video/mp4", []byte("video"))
	extractor := &scriptedSegmentedVideoExtractor{duration: 10 * 60, failSegment: 1}
	runtime := VideoExtractRuntime{
		Extractor: extractor, MaxSourceBytes: 1 << 20, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		Checkpoint: func(VideoExtractCheckpoint) error { return datastore.ErrSemanticTaskPayloadTooLarge },
	}
	task := VideoExtractTaskSpec{FileID: fileID, Path: "/vid/rejected-checkpoint.mp4", ContentType: "video/mp4", Revision: 1}

	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), task, runtime)
	if result != VideoExtractResultExtractError || err == nil {
		t.Fatalf("first attempt result=%q err=%v, want retryable segment failure", result, err)
	}
	result, err = b.ProcessVideoExtractTaskWithRuntime(context.Background(), task, runtime)
	if result != VideoExtractResultWritten || err != nil {
		t.Fatalf("retry result=%q err=%v, want written", result, err)
	}
	if got := fmt.Sprint(extractor.segmentCalls); got != "[0 1 0 1]" {
		t.Fatalf("segment calls=%s, want rejected cursor to restart from segment 0", got)
	}
}

func (e *staticVideoExtractor) ExtractVideoText(ctx context.Context, req VideoExtractRequest) (string, VideoExtractUsage, error) {
	e.calls++
	if e.err != nil {
		return "", VideoExtractUsage{}, e.err
	}
	if req.BeforeProviderCall != nil {
		req.BeforeProviderCall()
	}
	return e.text, e.usage, nil
}

func TestProcessVideoExtractTaskCheckpointsAndResumesVisualSegments(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/segmented.mp4", "video/mp4", []byte("video"))
	extractor := &scriptedSegmentedVideoExtractor{duration: 15 * 60, failSegment: 2}
	var checkpoint VideoExtractCheckpoint
	providerAttempts := 0
	checkpointWrites := 0
	runtime := VideoExtractRuntime{
		Extractor: extractor, MaxSourceBytes: 1 << 20, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		BeforeExtract: func() { providerAttempts++ },
		Checkpoint: func(next VideoExtractCheckpoint) error {
			checkpointWrites++
			checkpoint = next
			return nil
		},
	}
	task := VideoExtractTaskSpec{FileID: fileID, Path: "/vid/segmented.mp4", ContentType: "video/mp4", Revision: 1}

	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), task, runtime)
	if result != VideoExtractResultExtractError || err == nil {
		t.Fatalf("first attempt result=%q err=%v, want retryable segment failure", result, err)
	}
	if checkpoint.VisualCompletedSegments != 2 || checkpoint.VisualSegmentCount != 3 {
		t.Fatalf("checkpoint=%+v, want two of three visual segments durable", checkpoint)
	}
	if !strings.Contains(checkpoint.VisualSegmentsText, "segment 0") || !strings.Contains(checkpoint.VisualSegmentsText, "segment 1") {
		t.Fatalf("checkpoint summaries=%q, want completed segment summaries", checkpoint.VisualSegmentsText)
	}

	runtime.CheckpointState = checkpoint
	result, err = b.ProcessVideoExtractTaskWithRuntime(context.Background(), task, runtime)
	if result != VideoExtractResultWritten || err != nil {
		t.Fatalf("retry result=%q err=%v, want written", result, err)
	}
	if got := fmt.Sprint(extractor.segmentCalls); got != "[0 1 2 2]" {
		t.Fatalf("segment calls=%s, want retry to resume at segment 2", got)
	}
	if extractor.synthesisCalls != 1 || !checkpoint.VisualDone {
		t.Fatalf("synthesis calls=%d checkpoint=%+v, want one completed synthesis", extractor.synthesisCalls, checkpoint)
	}
	if providerAttempts != 2 {
		t.Fatalf("provider attempts=%d, want one central counter update per task attempt", providerAttempts)
	}
	if checkpointWrites != 4 {
		t.Fatalf("checkpoint writes=%d, want one per completed segment and one final visual checkpoint", checkpointWrites)
	}
	if checkpoint.VisualCompletedSegments != 0 || checkpoint.VisualSegmentCount != 0 || checkpoint.VisualSegmentsText != "" {
		t.Fatalf("final checkpoint retained intermediate visual progress: %+v", checkpoint)
	}
	for _, key := range []string{"scene_0", "scene_1", "scene_2"} {
		if checkpoint.VisualTags[key] != "present" {
			t.Fatalf("visual tags=%+v, want retained %s tag", checkpoint.VisualTags, key)
		}
	}
}

func TestProcessVideoExtractTaskSkipsSuccessfulEmptyTrailingVisualSegment(t *testing.T) {
	binDir := t.TempDir()
	framePath := filepath.Join(binDir, "frame.jpg")
	if err := os.WriteFile(framePath, testVideoJPEG(t, 64, 64), 0o600); err != nil {
		t.Fatal(err)
	}
	ffmpeg := `#!/bin/sh
out=""
for arg in "$@"; do out="$arg"; done
case "$*" in
  *"-ss 0.000 "*|*"-ss 299.900 "*)
    case "$out" in
      *%04d*) out=$(printf '%s' "$out" | sed 's/%04d/0001/') ;;
    esac
    cp "$FAKE_VIDEO_FRAME" "$out"
    ;;
esac
`
	ffmpegPath := filepath.Join(binDir, "ffmpeg")
	if err := os.WriteFile(ffmpegPath, []byte(ffmpeg), 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(binDir, "ffprobe"), []byte("#!/bin/sh\nprintf '600\\n'\n"), 0o700); err != nil {
		t.Fatal(err)
	}
	t.Setenv("FAKE_VIDEO_FRAME", framePath)

	imageRequests := 0
	synthesisRequests := 0
	provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		if err != nil {
			t.Errorf("read request: %v", err)
		}
		content := `{"caption_en":"opening"}`
		if bytes.Contains(body, []byte(`"type":"image_url"`)) {
			imageRequests++
		} else {
			synthesisRequests++
			content = `{"caption_en":"complete visual prefix"}`
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = fmt.Fprintf(w, `{"choices":[{"message":{"content":%q}}]}`, content)
	}))
	defer provider.Close()

	extractor, err := NewOpenAIVideoTextExtractor(OpenAIVideoTextExtractorConfig{
		BaseURL: provider.URL, APIKey: "key", Model: "vision", Client: provider.Client(), FFmpegPath: ffmpegPath,
	})
	if err != nil {
		t.Fatal(err)
	}
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/short-visual-long-audio.mp4", "video/mp4", []byte("video"))
	var checkpoints []VideoExtractCheckpoint
	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/short-visual-long-audio.mp4", ContentType: "video/mp4", Revision: 1,
	}, VideoExtractRuntime{
		Extractor: extractor, Timeout: time.Second, MaxSourceBytes: 1 << 20, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		Checkpoint: func(next VideoExtractCheckpoint) error {
			checkpoints = append(checkpoints, next)
			return nil
		},
	})
	if result != VideoExtractResultWritten || err != nil {
		t.Fatalf("result=%q err=%v, want decodable visual prefix written", result, err)
	}
	if imageRequests != 1 || synthesisRequests != 1 {
		t.Fatalf("image requests=%d synthesis requests=%d, want one prefix request and one synthesis", imageRequests, synthesisRequests)
	}
	if len(checkpoints) != 3 || checkpoints[1].VisualCompletedSegments != 2 || !checkpoints[2].VisualDone {
		t.Fatalf("checkpoints=%+v, want prefix, skipped-tail cursor, then visual completion", checkpoints)
	}
	if got := waitForContentText(t, b, "/vid/short-visual-long-audio.mp4", time.Second); !strings.Contains(got, "complete visual prefix") {
		t.Fatalf("content_text=%q, want synthesized visual prefix", got)
	}
}

func TestProcessVideoExtractTaskPreservesVisualCursorThroughAudioCheckpoint(t *testing.T) {
	installAudioFFmpegSegmentStub(t, 3)
	workspace, err := NewMediaExtractWorkspace(MediaExtractWorkspaceOptions{RootDir: t.TempDir()})
	if err != nil {
		t.Fatal(err)
	}
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/large-segmented-visual.mp4", "video/mp4", make([]byte, 100<<10))
	visual := &scriptedSegmentedVideoExtractor{
		duration:      10 * 60,
		failSegment:   -1,
		synthesisText: strings.Repeat("v", videoCheckpointTextMaxJSONBytes+(4<<10)),
	}
	audio := &failOnceAudioExtractor{failAt: 2}
	var checkpoint VideoExtractCheckpoint
	runtime := VideoExtractRuntime{
		Extractor: visual, MaxSourceBytes: 1 << 20, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		Workspace: workspace,
		AudioRuntime: &AudioExtractRuntime{
			Extractor: audio, MaxAudioBytes: 40 << 10, MaxExtractTextBytes: DefaultAudioExtractMaxTextBytes,
		},
		Checkpoint: func(next VideoExtractCheckpoint) error {
			checkpoint = next
			return nil
		},
	}
	task := VideoExtractTaskSpec{FileID: fileID, Path: "/vid/large-segmented-visual.mp4", ContentType: "video/mp4", Revision: 1}

	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), task, runtime)
	if result != VideoExtractResultExtractError || err == nil {
		t.Fatalf("first attempt result=%q err=%v, want retryable audio failure", result, err)
	}
	if checkpoint.VisualCompletedSegments != 2 || checkpoint.VisualSegmentCount != 2 {
		t.Fatalf("checkpoint=%+v, want durable visual cursor preserved by audio checkpoint", checkpoint)
	}
	if checkpoint.AudioCompletedSegments != 1 {
		t.Fatalf("audio completed segments=%d, want 1", checkpoint.AudioCompletedSegments)
	}

	runtime.CheckpointState = checkpoint
	result, err = b.ProcessVideoExtractTaskWithRuntime(context.Background(), task, runtime)
	if result != VideoExtractResultWritten || err != nil {
		t.Fatalf("retry result=%q err=%v, want written", result, err)
	}
	if got := fmt.Sprint(visual.segmentCalls); got != "[0 1]" {
		t.Fatalf("visual segment calls=%s, want retry to resume at synthesis", got)
	}
}

func TestProcessVideoExtractTaskRejectsTooLongBeforeProviderCall(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/too-long.mp4", "video/mp4", []byte("video"))
	extractor := &scriptedSegmentedVideoExtractor{duration: 2*60*60 + 1, failSegment: -1}
	providerCalls := 0
	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/too-long.mp4", ContentType: "video/mp4", Revision: 1,
	}, VideoExtractRuntime{
		Extractor: extractor, MaxSourceBytes: 1 << 20, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		BeforeExtract: func() { providerCalls++ },
	})
	if result != VideoExtractResultTooLong || err != nil {
		t.Fatalf("result=%q err=%v, want terminal too_long", result, err)
	}
	if providerCalls != 0 || len(extractor.segmentCalls) != 0 || extractor.synthesisCalls != 0 {
		t.Fatalf("provider calls=%d segments=%v synthesis=%d, want no provider work", providerCalls, extractor.segmentCalls, extractor.synthesisCalls)
	}
}

func TestProcessVideoExtractTaskRejectsTooLongWhenResumingAudio(t *testing.T) {
	workspace, err := NewMediaExtractWorkspace(MediaExtractWorkspaceOptions{RootDir: t.TempDir()})
	if err != nil {
		t.Fatal(err)
	}
	binDir := t.TempDir()
	ffprobe := `#!/bin/sh
case "$*" in
  *format=duration*) printf '7201\n' ;;
  *) printf '0\n' ;;
esac
`
	if err := os.WriteFile(filepath.Join(binDir, "ffprobe"), []byte(ffprobe), 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(binDir, "ffmpeg"), []byte("#!/bin/sh\nexit 99\n"), 0o700); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))

	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/resume-too-long.mp4", "video/mp4", []byte("video"))
	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/resume-too-long.mp4", ContentType: "video/mp4", Revision: 1,
	}, VideoExtractRuntime{
		Extractor: &staticVideoExtractor{text: "must not run"}, MaxSourceBytes: 1 << 20, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		Workspace:       workspace,
		CheckpointState: VideoExtractCheckpoint{VisualDone: true, VisualText: "[Visual Summary]\nvisual"},
		AudioRuntime:    &AudioExtractRuntime{Extractor: &staticAudioExtractor{text: "must not run"}, MaxAudioBytes: 7 << 20},
	})
	if result != VideoExtractResultTooLong || err != nil {
		t.Fatalf("result=%q err=%v, want terminal too_long", result, err)
	}
}

func TestProcessVideoExtractTaskTreatsProviderPayloadOverflowAsTerminal(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/payload-too-large.mp4", "video/mp4", []byte("video"))
	extractor := &staticVideoExtractor{err: ErrVideoProviderPayloadTooLarge}
	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/payload-too-large.mp4", ContentType: "video/mp4", Revision: 1,
	}, VideoExtractRuntime{
		Extractor: extractor, MaxSourceBytes: 1 << 20, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
	})
	if result != VideoExtractResultTooLarge || err != nil {
		t.Fatalf("result=%q err=%v, want terminal too_large", result, err)
	}
	if extractor.calls != 1 {
		t.Fatalf("extractor calls=%d, want one deterministic attempt", extractor.calls)
	}
}

func TestProcessVideoExtractTaskWithoutWorkspaceUsesProviderScaleMemoryLimit(t *testing.T) {
	b := newTestBackendWithS3AndOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertS3ImageFileForExtractTest(t, b, "/vid/no-workspace.mp4", "video/mp4", []byte("video"))
	if _, err := b.Store().DB().Exec(`UPDATE inodes SET size_bytes = ? WHERE inode_id = ?`, DefaultVideoExtractMaxProviderPayloadBytes+1, fileID); err != nil {
		t.Fatal(err)
	}
	extractor := &staticVideoExtractor{text: "must not run"}
	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/no-workspace.mp4", ContentType: "video/mp4", Revision: 1,
	}, VideoExtractRuntime{
		Extractor: extractor, MaxSourceBytes: 1 << 30, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
	})
	if result != VideoExtractResultLoadError || !errors.Is(err, ErrMediaExtractWorkspaceRequired) || extractor.calls != 0 {
		t.Fatalf("result=%q err=%v extractor calls=%d, want retryable workspace-required without provider call", result, err, extractor.calls)
	}
}

func TestProcessVideoExtractTaskAppliesTimeoutPerVisualRequest(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/per-request-timeout.mp4", "video/mp4", []byte("video"))
	extractor := &scriptedSegmentedVideoExtractor{duration: 5 * 60, failSegment: -1, callDelay: 25 * time.Millisecond, segmentOps: 2}
	start := time.Now()
	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/per-request-timeout.mp4", ContentType: "video/mp4", Revision: 1,
	}, VideoExtractRuntime{
		Extractor: extractor, Timeout: 40 * time.Millisecond, MaxSourceBytes: 1 << 20,
		MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
	})
	if result != VideoExtractResultWritten || err != nil {
		t.Fatalf("result=%q err=%v, want three individually bounded provider requests to succeed", result, err)
	}
	if elapsed := time.Since(start); elapsed < 75*time.Millisecond {
		t.Fatalf("elapsed=%v, want total visual phase longer than one request timeout", elapsed)
	}
}

func TestOpenAIVideoSegmentFFmpegUsesOperationTimeout(t *testing.T) {
	binDir := t.TempDir()
	ffmpeg := filepath.Join(binDir, "ffmpeg")
	if err := os.WriteFile(ffmpeg, []byte("#!/bin/sh\nexec sleep 10\n"), 0o700); err != nil {
		t.Fatal(err)
	}
	sourcePath := filepath.Join(t.TempDir(), "source.mp4")
	if err := os.WriteFile(sourcePath, []byte("video"), 0o600); err != nil {
		t.Fatal(err)
	}
	extractor, err := NewOpenAIVideoTextExtractor(OpenAIVideoTextExtractorConfig{
		BaseURL: "https://api.example.com", APIKey: "key", Model: "vision", Timeout: 50 * time.Millisecond, FFmpegPath: ffmpeg,
	})
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
	defer cancel()
	start := time.Now()
	_, _, err = extractor.ExtractVideoSegment(ctx, VideoExtractRequest{SourcePath: sourcePath, ContentType: "video/mp4"}, VideoVisualSegment{EndSeconds: 300})
	if err == nil {
		t.Fatal("ExtractVideoSegment succeeded with blocked ffmpeg")
	}
	if elapsed := time.Since(start); elapsed >= 300*time.Millisecond {
		t.Fatalf("ffmpeg elapsed=%v, want operation timeout before parent deadline", elapsed)
	}
}

func TestOpenAIVideoSynthesisUsesConfiguredOperationTimeout(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		time.Sleep(100 * time.Millisecond)
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"late"}}]}`))
	}))
	defer server.Close()
	extractor, err := NewOpenAIVideoTextExtractor(OpenAIVideoTextExtractorConfig{
		BaseURL: server.URL, APIKey: "key", Model: "vision", Timeout: 30 * time.Millisecond, Client: server.Client(),
	})
	if err != nil {
		t.Fatal(err)
	}
	start := time.Now()
	if _, _, err := extractor.SynthesizeVideoText(context.Background(), VideoExtractRequest{}, "summary"); err == nil {
		t.Fatal("SynthesizeVideoText succeeded after operation timeout")
	}
	if elapsed := time.Since(start); elapsed >= 90*time.Millisecond {
		t.Fatalf("provider elapsed=%v, want configured operation timeout", elapsed)
	}
}

func TestProcessVideoExtractTaskKeepsVisualOutputBeyondCheckpointBudget(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/large-visual.mp4", "video/mp4", []byte("video"))
	visual := strings.Repeat("v", videoCheckpointTextMaxJSONBytes+(4<<10))
	var checkpoint VideoExtractCheckpoint
	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/large-visual.mp4", ContentType: "video/mp4", Revision: 1,
	}, VideoExtractRuntime{
		Extractor: &staticVideoExtractor{text: visual}, MaxSourceBytes: 1 << 20,
		MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		Checkpoint: func(next VideoExtractCheckpoint) error {
			checkpoint = next
			return nil
		},
	})
	if result != VideoExtractResultWritten || err != nil {
		t.Fatalf("result=%q err=%v, want written", result, err)
	}
	got := waitForContentText(t, b, "/vid/large-visual.mp4", time.Second)
	if strings.Count(got, "v") != len(visual) {
		t.Fatalf("visual summary contains %d payload bytes, want %d", strings.Count(got, "v"), len(visual))
	}
	if checkpoint.VisualDone {
		t.Fatalf("oversized visual output marked durably complete with checkpoint=%+v", checkpoint)
	}
}

func TestProcessVideoExtractTaskDoesNotCountLocalVisualFailure(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{
		DatabaseAutoEmbedding: true,
		LLMCostBudget: LLMCostBudgetOptions{
			MaxMonthlyMillicents: -1, FallbackImageCostMillicents: 10,
		},
	})
	fileID := insertImageFileForExtractTest(t, b, "/vid/local-failure.mp4", "video/mp4", []byte("video"))
	cleanupVideoUsageForTask(t, b, fileID)
	providerCalls := 0
	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/local-failure.mp4", ContentType: "video/mp4", Revision: 1,
	}, VideoExtractRuntime{
		Extractor: &scriptedSegmentedVideoExtractor{duration: 5 * 60, failSegment: 0}, MaxSourceBytes: 1 << 20,
		MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		BeforeExtract:       func() { providerCalls++ },
	})
	if result != VideoExtractResultExtractError || err == nil {
		t.Fatalf("result=%q err=%v, want extract_error", result, err)
	}
	if providerCalls != 0 {
		t.Fatalf("provider calls=%d, want zero before-provider local failure", providerCalls)
	}
	assertVideoUsageCost(t, b, fileID, 0)
}

func TestProcessVideoExtractTaskRecordsVisualUsageBeforeCheckpointFailure(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{
		DatabaseAutoEmbedding: true,
		LLMCostBudget: LLMCostBudgetOptions{
			MaxMonthlyMillicents: -1, VisionCostPerKTokenMillicents: 1000,
		},
	})
	fileID := insertImageFileForExtractTest(t, b, "/vid/checkpoint-failure.mp4", "video/mp4", []byte("video"))
	cleanupVideoUsageForTask(t, b, fileID)
	checkpointErr := errors.New("checkpoint unavailable")
	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/checkpoint-failure.mp4", ContentType: "video/mp4", Revision: 1,
	}, VideoExtractRuntime{
		Extractor:      &staticVideoExtractor{text: "visual", usage: VideoExtractUsage{PromptTokens: 80, CompletionTokens: 20}},
		MaxSourceBytes: 1 << 20, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		Checkpoint: func(VideoExtractCheckpoint) error { return checkpointErr },
	})
	if result != VideoExtractResultExtractError || !errors.Is(err, checkpointErr) {
		t.Fatalf("result=%q err=%v, want extract_error checkpoint failure", result, err)
	}
	assertVideoUsageTokens(t, b, fileID, 100)
}

func cleanupVideoUsageForTask(t *testing.T, b *Dat9Backend, taskID string) {
	t.Helper()
	t.Cleanup(func() {
		if _, err := b.store.DB().Exec(`DELETE FROM llm_usage WHERE task_type = 'video_extract_visual' AND task_id = ?`, taskID); err != nil {
			t.Errorf("clean video usage for task %s: %v", taskID, err)
		}
	})
}

func assertVideoUsageTokens(t *testing.T, b *Dat9Backend, taskID string, want int64) {
	t.Helper()
	var got int64
	if err := b.store.DB().QueryRow(`SELECT COALESCE(SUM(raw_units), 0) FROM llm_usage WHERE task_type = 'video_extract_visual' AND task_id = ?`, taskID).Scan(&got); err != nil {
		t.Fatal(err)
	}
	if got != want {
		t.Fatalf("video usage tokens=%d, want %d", got, want)
	}
}

func assertVideoUsageCost(t *testing.T, b *Dat9Backend, taskID string, want int64) {
	t.Helper()
	var got int64
	if err := b.store.DB().QueryRow(`SELECT COALESCE(SUM(cost_millicents), 0) FROM llm_usage WHERE task_type = 'video_extract_visual' AND task_id = ?`, taskID).Scan(&got); err != nil {
		t.Fatal(err)
	}
	if got != want {
		t.Fatalf("video usage cost=%d, want %d", got, want)
	}
}

func TestProcessVideoExtractTaskRetriesOnlyFailedModality(t *testing.T) {
	workspace, err := NewMediaExtractWorkspace(MediaExtractWorkspaceOptions{RootDir: t.TempDir()})
	if err != nil {
		t.Fatal(err)
	}
	visual := &staticVideoExtractor{text: "bounded visual result"}
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/retry.mp4", "video/mp4", []byte("not-a-real-video"))
	var checkpoint VideoExtractCheckpoint
	runtime := VideoExtractRuntime{
		Extractor: visual, MaxSourceBytes: 1 << 20, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		Workspace:    workspace,
		AudioRuntime: &AudioExtractRuntime{Extractor: &staticAudioExtractor{text: "audio"}, MaxAudioBytes: 7 << 20},
		Checkpoint: func(next VideoExtractCheckpoint) error {
			checkpoint = next
			return nil
		},
	}
	task := VideoExtractTaskSpec{FileID: fileID, Path: "/vid/retry.mp4", ContentType: "video/mp4", Revision: 1}

	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), task, runtime)
	if err == nil || result != VideoExtractResultExtractError {
		t.Fatalf("first attempt result=%q err=%v, want retryable extract_error", result, err)
	}
	if !checkpoint.VisualDone || !strings.Contains(checkpoint.VisualText, "bounded visual result") {
		t.Fatalf("checkpoint=%+v, want completed visual phase", checkpoint)
	}
	if visual.calls != 1 {
		t.Fatalf("visual calls=%d, want 1", visual.calls)
	}

	runtime.CheckpointState = checkpoint
	runtime.FinalAttempt = true
	result, err = b.ProcessVideoExtractTaskWithRuntime(context.Background(), task, runtime)
	if err != nil || result != VideoExtractResultPartialSuccess {
		t.Fatalf("final attempt result=%q err=%v, want partial_success", result, err)
	}
	if visual.calls != 1 {
		t.Fatalf("visual calls=%d, want completed visual phase not repeated", visual.calls)
	}
	got := waitForContentText(t, b, "/vid/retry.mp4", time.Second)
	if !strings.Contains(got, "bounded visual result") {
		t.Fatalf("content_text=%q, want checkpointed visual result", got)
	}
}

func TestProcessVideoExtractTaskPreservesAudioProviderFailure(t *testing.T) {
	workspace, err := NewMediaExtractWorkspace(MediaExtractWorkspaceOptions{RootDir: t.TempDir()})
	if err != nil {
		t.Fatal(err)
	}
	binDir := t.TempDir()
	ffprobe := filepath.Join(binDir, "ffprobe")
	ffprobeScript := `#!/bin/sh
case "$*" in
  *format=duration*) printf '60\n' ;;
  *) printf '0\n' ;;
esac
`
	if err := os.WriteFile(ffprobe, []byte(ffprobeScript), 0o700); err != nil {
		t.Fatal(err)
	}
	ffmpeg := filepath.Join(binDir, "ffmpeg")
	if err := os.WriteFile(ffmpeg, []byte("#!/bin/sh\nout=\"\"\nfor arg in \"$@\"; do out=\"$arg\"; done\np=$(printf '%s' \"$out\" | sed 's/%03d/000/')\nprintf x > \"$p\"\n"), 0o700); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))

	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/audio-provider-error.mp4", "video/mp4", []byte("video"))
	runtime := VideoExtractRuntime{
		Extractor: &staticVideoExtractor{text: "must not run"}, MaxSourceBytes: 1 << 20, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		Workspace: workspace,
		CheckpointState: VideoExtractCheckpoint{
			VisualDone: true,
			VisualText: "[Visual Summary]\nvisual",
		},
		AudioRuntime: &AudioExtractRuntime{
			Extractor:     &staticAudioExtractor{err: &AudioExtractAPIError{Provider: "qwen asr", StatusCode: http.StatusUnauthorized}},
			MaxAudioBytes: 7 << 20,
		},
	}

	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/audio-provider-error.mp4", ContentType: "video/mp4", Revision: 1,
	}, runtime)
	if result != VideoExtractResultExtractError || err == nil {
		t.Fatalf("result=%q err=%v, want extract_error", result, err)
	}
	category, status, ok := ImageProviderFailure(err)
	if !ok || category != ImageProviderFailureAuthRejected || status != http.StatusUnauthorized {
		t.Fatalf("provider failure=(%q, %d, %t), want (%q, %d, true); err=%v", category, status, ok, ImageProviderFailureAuthRejected, http.StatusUnauthorized, err)
	}
}

func TestProcessVideoExtractTaskTimeoutBoundsFFprobe(t *testing.T) {
	workspace, err := NewMediaExtractWorkspace(MediaExtractWorkspaceOptions{RootDir: t.TempDir()})
	if err != nil {
		t.Fatal(err)
	}
	binDir := t.TempDir()
	if err := os.WriteFile(filepath.Join(binDir, "ffprobe"), []byte("#!/bin/sh\nwhile :; do sleep 1; done\n"), 0o700); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/blocked.mp4", "video/mp4", []byte("video"))
	start := time.Now()
	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/blocked.mp4", ContentType: "video/mp4", Revision: 1,
	}, VideoExtractRuntime{
		Extractor: &staticVideoExtractor{text: "unused"}, MaxSourceBytes: 1 << 20,
		MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes, Workspace: workspace,
		CheckpointState: VideoExtractCheckpoint{VisualDone: true, VisualText: "[Visual Summary]\nvisual"},
		AudioRuntime: &AudioExtractRuntime{
			Extractor: &staticAudioExtractor{text: "unused"}, Timeout: 50 * time.Millisecond, MaxAudioBytes: 7 << 20,
		},
	})
	if result != VideoExtractResultExtractError || !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("result=%q err=%v, want extract_error deadline exceeded", result, err)
	}
	if elapsed := time.Since(start); elapsed > 2*time.Second {
		t.Fatalf("ffprobe timeout took %v", elapsed)
	}
}

func TestProcessVideoExtractTaskKeepsAudioOutputBeyondCheckpointBudget(t *testing.T) {
	installVideoAudioCommandStubs(t, 3)
	workspace, err := NewMediaExtractWorkspace(MediaExtractWorkspaceOptions{RootDir: t.TempDir()})
	if err != nil {
		t.Fatal(err)
	}
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/large-audio.mp4", "video/mp4", []byte("video"))
	var checkpoint VideoExtractCheckpoint
	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/large-audio.mp4", ContentType: "video/mp4", Revision: 1,
	}, VideoExtractRuntime{
		Extractor: &staticVideoExtractor{text: "unused"}, MaxSourceBytes: 1 << 20,
		MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes, Workspace: workspace,
		CheckpointState: VideoExtractCheckpoint{VisualDone: true, VisualText: "[Visual Summary]\nvisual"},
		Checkpoint: func(next VideoExtractCheckpoint) error {
			checkpoint = next
			return nil
		},
		AudioRuntime: &AudioExtractRuntime{
			Extractor: &staticAudioExtractor{text: strings.Repeat("x", 8<<10)}, MaxAudioBytes: 7 << 20,
			MaxExtractTextBytes: DefaultAudioExtractMaxTextBytes,
		},
	})
	if result != VideoExtractResultWritten || err != nil {
		t.Fatalf("result=%q err=%v, want written", result, err)
	}
	got := waitForContentText(t, b, "/vid/large-audio.mp4", time.Second)
	if strings.Count(got, "x") < 24<<10 {
		t.Fatalf("audio transcript contains %d payload bytes, want all three segments", strings.Count(got, "x"))
	}
	if checkpoint.AudioDone {
		t.Fatalf("oversized audio output marked durably complete with checkpoint=%+v", checkpoint)
	}
}

func TestProcessVideoExtractTaskRecordsAudioUsageBeforeRetry(t *testing.T) {
	installVideoAudioCommandStubs(t, 3)
	workspace, err := NewMediaExtractWorkspace(MediaExtractWorkspaceOptions{RootDir: t.TempDir()})
	if err != nil {
		t.Fatal(err)
	}
	extractor := &meteredFailOnceAudioExtractor{}
	b := newTestBackendWithOptions(t, Options{
		DatabaseAutoEmbedding: true,
		LLMCostBudget: LLMCostBudgetOptions{
			MaxMonthlyMillicents: -1, WhisperCostPerMinuteMillicents: 60, FallbackAudioCostMillicents: 10,
		},
	})
	fileID := insertImageFileForExtractTest(t, b, "/vid/metered-audio.mp4", "video/mp4", []byte("video"))
	cleanupAudioUsageForTask(t, b, fileID)
	var checkpoint VideoExtractCheckpoint
	runtime := VideoExtractRuntime{
		Extractor: &staticVideoExtractor{text: "unused"}, MaxSourceBytes: 1 << 20,
		MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes, Workspace: workspace,
		CheckpointState: VideoExtractCheckpoint{VisualDone: true, VisualText: "[Visual Summary]\nvisual"},
		Checkpoint: func(next VideoExtractCheckpoint) error {
			checkpoint = next
			return nil
		},
		AudioRuntime: &AudioExtractRuntime{
			Extractor: extractor, MaxAudioBytes: 7 << 20, MaxExtractTextBytes: DefaultAudioExtractMaxTextBytes,
		},
	}
	task := VideoExtractTaskSpec{FileID: fileID, Path: "/vid/metered-audio.mp4", ContentType: "video/mp4", Revision: 1}
	if result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), task, runtime); result != VideoExtractResultExtractError || err == nil {
		t.Fatalf("first result=%q err=%v, want retryable extract_error", result, err)
	}
	assertAudioUsageMillis(t, b, fileID, 60_000)

	runtime.CheckpointState = checkpoint
	if result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), task, runtime); result != VideoExtractResultWritten || err != nil {
		t.Fatalf("retry result=%q err=%v, want written", result, err)
	}
	assertAudioUsageMillis(t, b, fileID, 180_000)
}

func TestExtractVideoAudioTrackCheckpointsCompletedSegmentTags(t *testing.T) {
	installVideoAudioCommandStubs(t, 2)
	workspace, err := NewMediaExtractWorkspace(MediaExtractWorkspaceOptions{RootDir: t.TempDir()})
	if err != nil {
		t.Fatal(err)
	}
	taskWorkspace, err := workspace.NewTask(context.Background(), "video-audio-tags")
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = taskWorkspace.Close() })
	sourcePath := filepath.Join(taskWorkspace.Dir(), "source.mp4")
	if err := os.WriteFile(sourcePath, []byte("video"), 0o600); err != nil {
		t.Fatal(err)
	}

	extractor := &sequenceAudioExtractor{
		responses: []string{`{"text":"first","attributes":{"segment":"one"}}`, ""},
		errs:      []error{nil, errors.New("temporary provider failure")},
	}
	var checkpoint AudioExtractCheckpoint
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	_, _, _, _, err = b.extractVideoAudioTrack(context.Background(), VideoExtractTaskSpec{
		FileID: "video-audio-tags", Path: "/video/audio-tags.mp4", Revision: 1,
	}, sourcePath, nil, "video/mp4", 1200, taskWorkspace, AudioExtractRuntime{
		Extractor: extractor, MaxAudioBytes: 7 << 20, MaxExtractTextBytes: DefaultAudioExtractMaxTextBytes,
		Checkpoint: func(next AudioExtractCheckpoint) error {
			checkpoint = next
			return nil
		},
	})
	if err == nil {
		t.Fatal("extractVideoAudioTrack error=nil, want second-segment failure")
	}
	if checkpoint.CompletedSegments != 1 || checkpoint.Tags["segment"] != "one" {
		t.Fatalf("checkpoint=%+v, want first segment text and tags durable", checkpoint)
	}
	retryRuntime := AudioExtractRuntime{
		Extractor:     &sequenceAudioExtractor{responses: []string{`{"text":"second","attributes":{"language":"en"}}`}},
		MaxAudioBytes: 7 << 20, MaxExtractTextBytes: DefaultAudioExtractMaxTextBytes,
		CheckpointState: checkpoint,
	}
	_, _, tags, _, err := b.extractVideoAudioTrack(context.Background(), VideoExtractTaskSpec{
		FileID: "video-audio-tags", Path: "/video/audio-tags.mp4", Revision: 1,
	}, sourcePath, nil, "video/mp4", 1200, taskWorkspace, retryRuntime)
	if err != nil {
		t.Fatal(err)
	}
	if tags["segment"] != "one" || tags["language"] != "en" {
		t.Fatalf("retry tags=%+v, want completed and resumed segment tags", tags)
	}
}

func TestExtractVideoAudioTrackRejectsKnownDurationBeyondAudioLimit(t *testing.T) {
	workspace, err := NewMediaExtractWorkspace(MediaExtractWorkspaceOptions{RootDir: t.TempDir()})
	if err != nil {
		t.Fatal(err)
	}
	taskWorkspace, err := workspace.NewTask(context.Background(), "video-audio-too-long")
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = taskWorkspace.Close() })
	sourcePath := filepath.Join(taskWorkspace.Dir(), "source.mp4")
	if err := os.WriteFile(sourcePath, []byte("video"), 0o600); err != nil {
		t.Fatal(err)
	}

	binDir := t.TempDir()
	if err := os.WriteFile(filepath.Join(binDir, "ffprobe"), []byte("#!/bin/sh\nprintf '0\\n'\n"), 0o700); err != nil {
		t.Fatal(err)
	}
	ffmpegMarker := filepath.Join(binDir, "ffmpeg-called")
	ffmpeg := fmt.Sprintf("#!/bin/sh\n: > %q\nexit 99\n", ffmpegMarker)
	if err := os.WriteFile(filepath.Join(binDir, "ffmpeg"), []byte(ffmpeg), 0o700); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))

	b := &Dat9Backend{}
	_, _, _, present, err := b.extractVideoAudioTrack(
		context.Background(),
		VideoExtractTaskSpec{FileID: "video-audio-too-long", Path: "/video/too-long.mp4", Revision: 1},
		sourcePath,
		nil,
		"video/mp4",
		float64(maxAudioExtractDurationSeconds)+1,
		taskWorkspace,
		AudioExtractRuntime{Extractor: &staticAudioExtractor{text: "must not run"}, MaxAudioBytes: 7 << 20},
	)
	if !present || !errors.Is(err, ErrAudioExtractDurationTooLong) {
		t.Fatalf("present=%t error=%v, want present audio rejected as too long", present, err)
	}
	if _, err := os.Stat(ffmpegMarker); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("ffmpeg marker error=%v, want no FFmpeg work", err)
	}
}

func TestExtractVideoAudioTrackUsesAbsoluteAudioRanges(t *testing.T) {
	installVideoAudioCommandStubs(t, 2)
	workspace, err := NewMediaExtractWorkspace(MediaExtractWorkspaceOptions{RootDir: t.TempDir()})
	if err != nil {
		t.Fatal(err)
	}
	taskWorkspace, err := workspace.NewTask(context.Background(), "video-audio-timeline")
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = taskWorkspace.Close() })
	sourcePath := filepath.Join(taskWorkspace.Dir(), "source.mp4")
	if err := os.WriteFile(sourcePath, []byte("video"), 0o600); err != nil {
		t.Fatal(err)
	}

	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	text, _, _, present, err := b.extractVideoAudioTrack(
		context.Background(),
		VideoExtractTaskSpec{FileID: "video-audio-timeline", Path: "/video/timeline.mp4", Revision: 1},
		sourcePath,
		nil,
		"video/mp4",
		650,
		taskWorkspace,
		AudioExtractRuntime{
			Extractor:           &sequenceAudioExtractor{responses: []string{"opening", "ending"}},
			MaxAudioBytes:       7 << 20,
			MaxExtractTextBytes: DefaultAudioExtractMaxTextBytes,
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	if !present {
		t.Fatal("audio track reported absent")
	}
	want := "[Audio 00:00:00-00:10:00]\nopening\n" +
		"[Audio 00:10:00-00:10:50]\nending"
	if text != want {
		t.Fatalf("text=%q, want %q", text, want)
	}
}

func installVideoAudioCommandStubs(t *testing.T, segmentCount int) {
	t.Helper()
	binDir := t.TempDir()
	ffprobe := `#!/bin/sh
case "$*" in
  *format=duration*) printf '1200\n' ;;
  *) printf '0\n' ;;
esac
`
	if err := os.WriteFile(filepath.Join(binDir, "ffprobe"), []byte(ffprobe), 0o700); err != nil {
		t.Fatal(err)
	}
	script := fmt.Sprintf(`#!/bin/sh
out=""
for arg in "$@"; do out="$arg"; done
i=0
while [ "$i" -lt %d ]; do
  suffix=$(printf '%%03d' "$i")
  path=$(printf '%%s' "$out" | sed "s/%%03d/$suffix/")
  printf x > "$path"
  i=$((i + 1))
done
`, segmentCount)
	if err := os.WriteFile(filepath.Join(binDir, "ffmpeg"), []byte(script), 0o700); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
}

func TestProcessVideoExtractTaskCountsOnlyNewVisualExtractionAttempts(t *testing.T) {
	visual := &staticVideoExtractor{text: "visual"}
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/charge.mp4", "video/mp4", []byte("video"))
	charges := 0
	runtime := VideoExtractRuntime{
		Extractor: visual, MaxSourceBytes: 1 << 20, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		BeforeExtract: func() { charges++ },
	}
	task := VideoExtractTaskSpec{FileID: fileID, Path: "/vid/charge.mp4", ContentType: "video/mp4"}
	if result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), task, runtime); err != nil || result != VideoExtractResultWritten {
		t.Fatalf("first result=%q err=%v", result, err)
	}
	if charges != 1 || visual.calls != 1 {
		t.Fatalf("charges=%d visual_calls=%d, want 1/1", charges, visual.calls)
	}

	fileID = insertImageFileForExtractTest(t, b, "/vid/resume.mp4", "video/mp4", []byte("video"))
	task = VideoExtractTaskSpec{FileID: fileID, Path: "/vid/resume.mp4", ContentType: "video/mp4"}
	runtime.CheckpointState = VideoExtractCheckpoint{VisualDone: true, VisualText: "[Visual Summary]\nvisual"}
	if result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), task, runtime); err != nil || result != VideoExtractResultWritten {
		t.Fatalf("resume result=%q err=%v", result, err)
	}
	if charges != 1 || visual.calls != 1 {
		t.Fatalf("resume charges=%d visual_calls=%d, want unchanged 1/1", charges, visual.calls)
	}
}

func TestProcessVideoExtractTaskCountsLegacyProviderOncePerAttempt(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	fileID := insertImageFileForExtractTest(t, b, "/vid/legacy-multi-call.mp4", "video/mp4", []byte("video"))
	providerAttempts := 0
	result, err := b.ProcessVideoExtractTaskWithRuntime(context.Background(), VideoExtractTaskSpec{
		FileID: fileID, Path: "/vid/legacy-multi-call.mp4", ContentType: "video/mp4", Revision: 1,
	}, VideoExtractRuntime{
		Extractor: multiCallVideoExtractor{}, MaxSourceBytes: 1 << 20, MaxExtractTextBytes: DefaultVideoExtractMaxTextBytes,
		BeforeExtract: func() { providerAttempts++ },
	})
	if result != VideoExtractResultWritten || err != nil {
		t.Fatalf("result=%q err=%v, want written", result, err)
	}
	if providerAttempts != 1 {
		t.Fatalf("provider attempts=%d, want one per task attempt", providerAttempts)
	}
}

func TestProcessVideoExtractTaskWritesContentText(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:   true,
			Extractor: &staticVideoExtractor{text: "a dog running in a park with trees"},
		},
	})
	fileID := insertImageFileForExtractTest(t, b, "/vid/clip.mp4", "video/mp4", []byte{0x00, 0x00, 0x00, 0x18})
	result, err := b.ProcessVideoExtractTask(context.Background(), VideoExtractTaskSpec{
		FileID:      fileID,
		Path:        "/vid/clip.mp4",
		ContentType: "video/mp4",
		Revision:    1,
	})
	if err != nil {
		t.Fatal(err)
	}
	if result != VideoExtractResultWritten {
		t.Fatalf("result=%q, want %q", result, VideoExtractResultWritten)
	}
	got := waitForContentText(t, b, "/vid/clip.mp4", time.Second)
	if !strings.Contains(got, "dog") {
		t.Fatalf("unexpected content_text: %q", got)
	}
}

func TestProcessVideoExtractTaskNotConfigured(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{
		DatabaseAutoEmbedding: true,
	})
	result, err := b.ProcessVideoExtractTask(context.Background(), VideoExtractTaskSpec{
		FileID:      "non-existent",
		Path:        "/vid/clip.mp4",
		ContentType: "video/mp4",
		Revision:    1,
	})
	if err == nil {
		t.Fatal("expected error for unconfigured runtime")
	}
	if result != VideoExtractResultRuntimeNotConfigured {
		t.Fatalf("result=%q, want %q", result, VideoExtractResultRuntimeNotConfigured)
	}
}

func TestProcessVideoExtractTaskNotVideo(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:   true,
			Extractor: &staticVideoExtractor{text: "should not be called"},
		},
	})
	fileID := insertImageFileForExtractTest(t, b, "/pic/img.png", "image/png", []byte{0x89, 0x50, 0x4e})
	result, err := b.ProcessVideoExtractTask(context.Background(), VideoExtractTaskSpec{
		FileID:      fileID,
		Path:        "/pic/img.png",
		ContentType: "image/png",
		Revision:    1,
	})
	if err != nil {
		t.Fatal(err)
	}
	if result != VideoExtractResultNotVideo {
		t.Fatalf("result=%q, want %q", result, VideoExtractResultNotVideo)
	}
}

func TestProcessVideoExtractTaskFileNotFound(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:   true,
			Extractor: &staticVideoExtractor{text: "should not be called"},
		},
	})
	result, err := b.ProcessVideoExtractTask(context.Background(), VideoExtractTaskSpec{
		FileID:      "non-existent-id",
		Path:        "/vid/clip.mp4",
		ContentType: "video/mp4",
		Revision:    1,
	})
	if err != nil {
		t.Fatal(err)
	}
	if result != VideoExtractResultFileNotFound {
		t.Fatalf("result=%q, want %q", result, VideoExtractResultFileNotFound)
	}
}

func TestProcessVideoExtractTaskStaleRevision(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:   true,
			Extractor: &staticVideoExtractor{text: "should not be called"},
		},
	})
	fileID := insertImageFileForExtractTest(t, b, "/vid/stale.mp4", "video/mp4", []byte{0x00, 0x00})
	result, err := b.ProcessVideoExtractTask(context.Background(), VideoExtractTaskSpec{
		FileID:      fileID,
		Path:        "/vid/stale.mp4",
		ContentType: "video/mp4",
		Revision:    999,
	})
	if err != nil {
		t.Fatal(err)
	}
	if result != VideoExtractResultStale {
		t.Fatalf("result=%q, want %q", result, VideoExtractResultStale)
	}
}

func TestEffectiveVideoMIMEAllowlist(t *testing.T) {
	tests := []struct {
		path, ct, want string
	}{
		{"/x.mp4", "video/mp4", "video/mp4"},
		{"/x.mov", "video/quicktime", "video/quicktime"},
		{"/x.avi", "video/x-msvideo", "video/x-msvideo"},
		{"/x.webm", "video/webm", "video/webm"},
		{"/x.mkv", "video/x-matroska", "video/x-matroska"},
		// path fallback when content_type is empty
		{"/x.mp4", "", "video/mp4"},
		{"/x.webm", "", "video/webm"},
		// non-video should return empty
		{"/x.png", "image/png", ""},
		{"/x.mp3", "audio/mpeg", ""},
		{"/x.txt", "text/plain", ""},
	}
	for _, tt := range tests {
		got := effectiveVideoMIME(tt.path, tt.ct)
		if got != tt.want {
			t.Errorf("effectiveVideoMIME(%q, %q) = %q, want %q", tt.path, tt.ct, got, tt.want)
		}
	}
}

func TestIsVideoContentType(t *testing.T) {
	if !isVideoContentType("video/mp4", "/x.mp4") {
		t.Fatal("expected video/mp4 to be video")
	}
	if isVideoContentType("audio/mpeg", "/x.mp3") {
		t.Fatal("expected audio/mpeg to not be video")
	}
	if !isVideoContentType("", "/x.webm") {
		t.Fatal("expected .webm path fallback to work")
	}
}

func TestVideoMIMEToExt(t *testing.T) {
	tests := []struct {
		mime, want string
	}{
		{"video/mp4", ".mp4"},
		{"video/quicktime", ".mov"},
		{"video/x-msvideo", ".avi"},
		{"video/webm", ".webm"},
		{"video/x-matroska", ".mkv"},
		{"unknown/type", ".mp4"},
	}
	for _, tt := range tests {
		got := videoMIMEToExt(tt.mime)
		if got != tt.want {
			t.Errorf("videoMIMEToExt(%q) = %q, want %q", tt.mime, got, tt.want)
		}
	}
}

func TestShouldEnqueueVideoExtractTask(t *testing.T) {
	// Not configured — should not enqueue
	b := newTestBackendWithOptions(t, Options{DatabaseAutoEmbedding: true})
	if b.shouldEnqueueVideoExtractTask("/x.mp4", "video/mp4") {
		t.Fatal("should not enqueue when video extract not configured")
	}

	// Configured with allowlisted tenant — video types should enqueue
	b2 := newTestBackendWithOptions(t, Options{
		TenantID:              "test-tenant",
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:         true,
			Extractor:       &staticVideoExtractor{text: "test"},
			TenantAllowlist: map[string]struct{}{"test-tenant": {}},
		},
	})
	if !b2.shouldEnqueueVideoExtractTask("/x.mp4", "video/mp4") {
		t.Fatal("should enqueue for video/mp4")
	}
	if b2.shouldEnqueueVideoExtractTask("/x.png", "image/png") {
		t.Fatal("should not enqueue for image/png")
	}
}

func TestVideoExtractTenantAllowlist(t *testing.T) {
	// Allowlist set — only listed tenant can enqueue.
	allowed := newTestBackendWithOptions(t, Options{
		TenantID:              "tenant-abc",
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:         true,
			Extractor:       &staticVideoExtractor{text: "test"},
			TenantAllowlist: map[string]struct{}{"tenant-abc": {}},
		},
	})
	if !allowed.shouldEnqueueVideoExtractTask("/x.mp4", "video/mp4") {
		t.Fatal("allowlisted tenant should enqueue video extract")
	}

	// Non-allowlisted tenant — should NOT enqueue.
	denied := newTestBackendWithOptions(t, Options{
		TenantID:              "tenant-xyz",
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:         true,
			Extractor:       &staticVideoExtractor{text: "test"},
			TenantAllowlist: map[string]struct{}{"tenant-abc": {}},
		},
	})
	if denied.shouldEnqueueVideoExtractTask("/x.mp4", "video/mp4") {
		t.Fatal("non-allowlisted tenant should not enqueue video extract")
	}

	// Nil allowlist (fail-closed) — no tenant allowed.
	nilList := newTestBackendWithOptions(t, Options{
		TenantID:              "tenant-any",
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:   true,
			Extractor: &staticVideoExtractor{text: "test"},
			// TenantAllowlist nil = fail-closed, no tenant allowed
		},
	})
	if nilList.shouldEnqueueVideoExtractTask("/x.mp4", "video/mp4") {
		t.Fatal("nil allowlist should deny all tenants (fail-closed)")
	}

	// Empty allowlist — no tenant allowed.
	none := newTestBackendWithOptions(t, Options{
		TenantID:              "tenant-any",
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:         true,
			Extractor:       &staticVideoExtractor{text: "test"},
			TenantAllowlist: map[string]struct{}{},
		},
	})
	if none.shouldEnqueueVideoExtractTask("/x.mp4", "video/mp4") {
		t.Fatal("empty allowlist should deny all tenants")
	}

	// AllTenants ("*") — any tenant allowed.
	wildcard := newTestBackendWithOptions(t, Options{
		TenantID:              "tenant-any",
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:    true,
			AllTenants: true,
			Extractor:  &staticVideoExtractor{text: "test"},
		},
	})
	if !wildcard.shouldEnqueueVideoExtractTask("/x.mp4", "video/mp4") {
		t.Fatal("AllTenants=true should allow any tenant")
	}
}

func TestVideoLLMQuotaDefault(t *testing.T) {
	// Verify default MaxVideoLLMFiles is 30 when not explicitly set.
	b := newTestBackendWithOptions(t, Options{
		TenantID:              "test-tenant",
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:    true,
			AllTenants: true,
			Extractor:  &staticVideoExtractor{text: "test"},
			// MaxVideoLLMFiles not set → should use default 30
		},
	})
	if b.maxVideoLLMFiles != 30 {
		t.Fatalf("maxVideoLLMFiles=%d, want 30", b.maxVideoLLMFiles)
	}
}

func TestVideoLLMQuotaCustom(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{
		TenantID:              "test-tenant",
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:          true,
			AllTenants:       true,
			Extractor:        &staticVideoExtractor{text: "test"},
			MaxVideoLLMFiles: 10,
		},
	})
	if b.maxVideoLLMFiles != 10 {
		t.Fatalf("maxVideoLLMFiles=%d, want 10", b.maxVideoLLMFiles)
	}
}

func TestVideoLLMQuotaEnforcementUsesCentralConfig(t *testing.T) {
	b, fake := newCentralQuotaBackend(t)
	fake.config["tenant-a"].MaxVideoLLMFiles = 2
	ctx := context.Background()

	insertVideoTask := func(tx *sql.Tx, id string) error {
		now := time.Now().UTC()
		_, err := tx.ExecContext(ctx,
			`INSERT INTO semantic_tasks
			 (task_id, task_type, resource_id, resource_version, status, attempt_count, max_attempts,
			  receipt, payload_json, created_at, updated_at)
			 VALUES (?, 'video_extract_visual', ?, ?, 'queued', 0, 3,
			         ?, '{}', ?, ?)`,
			id, "file-"+id, 1, id[:8], now, now)
		return err
	}

	err := b.store.InTx(ctx, func(tx *sql.Tx) error {
		if err := insertVideoTask(tx, uuid.NewString()); err != nil {
			return err
		}
		if b.videoLLMQuotaExceededTx(ctx, tx) {
			t.Error("quota should not be exceeded at 1 task with limit=2 from central config")
		}
		if err := insertVideoTask(tx, uuid.NewString()); err != nil {
			return err
		}
		if !b.videoLLMQuotaExceededTx(ctx, tx) {
			t.Error("quota should be exceeded at 2 tasks with limit=2 from central config")
		}
		return nil
	})
	if err != nil {
		t.Fatalf("transaction failed: %v", err)
	}
}

func TestVideoLLMQuotaExplicitZeroDisablesLimit(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:    true,
			AllTenants: true,
			Extractor:  &staticVideoExtractor{text: "test"},
		},
	})
	if b.maxVideoLLMFiles != 30 {
		t.Fatalf("process maxVideoLLMFiles=%d, want 30", b.maxVideoLLMFiles)
	}
	ctx := context.Background()
	fake := newFakeMetaQuotaStore()
	fake.config["tenant-a"] = &QuotaConfigView{TenantID: "tenant-a", MaxVideoLLMFiles: 0}
	b.SetMetaQuotaStore(ctx, "tenant-a", fake)

	err := b.store.InTx(ctx, func(tx *sql.Tx) error {
		now := time.Now().UTC()
		for range 50 {
			id := uuid.NewString()
			if _, err := tx.ExecContext(ctx,
				`INSERT INTO semantic_tasks
				 (task_id, task_type, resource_id, resource_version, status, attempt_count, max_attempts,
				  receipt, payload_json, created_at, updated_at)
				 VALUES (?, 'video_extract_visual', ?, ?, 'queued', 0, 3,
				         ?, '{}', ?, ?)`,
				id, "file-"+id, 1, id[:8], now, now); err != nil {
				return err
			}
		}
		if b.videoLLMQuotaExceededTx(ctx, tx) {
			t.Error("explicit zero central quota should disable the process-level video LLM limit")
		}
		return nil
	})
	if err != nil {
		t.Fatalf("transaction failed: %v", err)
	}
}

func TestParseVideoExtractTenantAllowlist(t *testing.T) {
	tests := []struct {
		name     string
		raw      string
		wantAll  bool
		wantList map[string]struct{}
		wantErr  bool
	}{
		{"empty", "", false, nil, false},
		{"whitespace only", "  ", false, nil, false},
		{"single tenant", "tenant-a", false, map[string]struct{}{"tenant-a": {}}, false},
		{"multi tenant", "tenant-a,tenant-b", false, map[string]struct{}{"tenant-a": {}, "tenant-b": {}}, false},
		{"multi tenant with spaces", " tenant-a , tenant-b ", false, map[string]struct{}{"tenant-a": {}, "tenant-b": {}}, false},
		{"wildcard all", "*", true, nil, false},
		{"wildcard with spaces", " * ", true, nil, false},
		{"trailing comma", "tenant-a,", false, map[string]struct{}{"tenant-a": {}}, false},
		{"only commas", ",", false, nil, false},
		{"commas and spaces", ", ,", false, nil, false},
		{"interior empty token", "tenant-a,,tenant-b", false, map[string]struct{}{"tenant-a": {}, "tenant-b": {}}, false},
		{"mixed wildcard and tenant", "*,tenant-a", false, nil, true},
		{"glob prefix pattern", "tenant-*", false, nil, true},
		{"glob in middle", "ten*ant", false, nil, true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotAll, gotList, err := ParseVideoExtractTenantAllowlist(tt.raw)
			if (err != nil) != tt.wantErr {
				t.Fatalf("err=%v, wantErr=%v", err, tt.wantErr)
			}
			if err != nil {
				return
			}
			if gotAll != tt.wantAll {
				t.Fatalf("allTenants=%v, want %v", gotAll, tt.wantAll)
			}
			if tt.wantList == nil && gotList != nil {
				t.Fatalf("allowlist=%v, want nil", gotList)
			}
			if tt.wantList != nil {
				if len(gotList) != len(tt.wantList) {
					t.Fatalf("allowlist len=%d, want %d", len(gotList), len(tt.wantList))
				}
				for k := range tt.wantList {
					if _, ok := gotList[k]; !ok {
						t.Fatalf("allowlist missing %q", k)
					}
				}
			}
		})
	}
}

func TestMP4VideoExcludesAudioEnqueue(t *testing.T) {
	// When both video and audio extract are enabled, MP4 files should only
	// enqueue video_extract_visual, not audio_extract_text, to avoid
	// dual content_text overwrites.
	b := newTestBackendWithOptions(t, Options{
		TenantID:              "test-tenant",
		DatabaseAutoEmbedding: true,
		AsyncVideoExtract: AsyncVideoExtractOptions{
			Enabled:         true,
			Extractor:       &staticVideoExtractor{text: "visual content"},
			TenantAllowlist: map[string]struct{}{"test-tenant": {}},
		},
		AsyncAudioExtract: AsyncAudioExtractOptions{
			Enabled:   true,
			Extractor: &staticAudioExtractor{text: "audio transcript"},
		},
	})

	// video/mp4 should enqueue video, not audio
	if !b.shouldEnqueueVideoExtractTask("/clip.mp4", "video/mp4") {
		t.Fatal("video/mp4 should enqueue video extract")
	}
	// The enqueue logic skips audio when video is active for the same file
	isVideo := b.shouldEnqueueVideoExtractTask("/clip.mp4", "video/mp4")
	isAudio := !isVideo && b.shouldEnqueueAudioExtractTask("/clip.mp4", "video/mp4")
	if isAudio {
		t.Fatal("MP4 should NOT enqueue audio when video extract is enabled — dual content_text overwrite")
	}

	// Pure audio files (mp3) should still enqueue audio
	isVideoMP3 := b.shouldEnqueueVideoExtractTask("/song.mp3", "audio/mpeg")
	isAudioMP3 := !isVideoMP3 && b.shouldEnqueueAudioExtractTask("/song.mp3", "audio/mpeg")
	if isVideoMP3 {
		t.Fatal("audio/mpeg should not enqueue video extract")
	}
	if !isAudioMP3 {
		t.Fatal("audio/mpeg should still enqueue audio extract")
	}
}
