package vision

import (
	"bytes"
	"context"
	"errors"
	"image"
	"image/png"
	"testing"

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

type fakeProvider struct {
	reply   string
	err     error
	seen    int
	lastQ   string
	lastDim int
}

func (f *fakeProvider) Describe(_ context.Context, im media.Image, prompt string) (string, error) {
	f.seen++
	f.lastQ = prompt
	f.lastDim = im.Width
	return f.reply, f.err
}

func pngPayload(t *testing.T, w, h int) []byte {
	t.Helper()
	var buf bytes.Buffer
	if err := png.Encode(&buf, image.NewRGBA(image.Rect(0, 0, w, h))); err != nil {
		t.Fatal(err)
	}
	return buf.Bytes()
}

func testIntake(t *testing.T, p Provider) (*Intake, *media.Store) {
	t.Helper()
	s, err := media.Open(t.TempDir(), 0, 0)
	if err != nil {
		t.Fatal(err)
	}
	return NewIntake(s, p, 64), s
}

func TestAcceptStoresThenDescribes(t *testing.T) {
	fp := &fakeProvider{reply: "кот на подоконнике"}
	in, store := testIntake(t, fp)

	res, err := in.Accept(context.Background(), pngPayload(t, 200, 100), "telegram", "кто это?")
	if err != nil {
		t.Fatalf("accept: %v", err)
	}
	if res.Description != "кот на подоконнике" {
		t.Errorf("description = %q", res.Description)
	}
	if fp.lastQ != "кто это?" {
		t.Errorf("question not passed through: %q", fp.lastQ)
	}
	if fp.lastDim != 64 {
		t.Errorf("image not downscaled to maxDim: width %d", fp.lastDim)
	}
	// The sniffed mime wins over anything a sender claimed.
	got, _, err := store.Read(res.Blob.ID)
	if err != nil {
		t.Fatalf("blob not stored: %v", err)
	}
	if got.MIME != "image/png" || got.Source != "telegram" {
		t.Errorf("blob metadata = %+v", got)
	}
}

// The ordering promise: with no vision model on the box — today's real state —
// the image is still on disk and the id is still reported, so it can be
// described later instead of being lost.
func TestAcceptKeepsBlobWhenDescribeFails(t *testing.T) {
	in, store := testIntake(t, Disabled{})
	res, err := in.Accept(context.Background(), pngPayload(t, 32, 32), "web:upload", "")
	if !errors.Is(err, ErrDisabled) {
		t.Fatalf("got %v, want ErrDisabled", err)
	}
	if res.Blob.ID == "" {
		t.Fatal("no blob id reported on a description failure")
	}
	if _, _, err := store.Read(res.Blob.ID); err != nil {
		t.Errorf("blob was not kept: %v", err)
	}
}

func TestRerunDescribesAStoredBlob(t *testing.T) {
	fp := &fakeProvider{reply: "текст: ошибка E24"}
	in, _ := testIntake(t, fp)
	first, err := in.Accept(context.Background(), pngPayload(t, 40, 40), "telegram", "")
	if err != nil {
		t.Fatal(err)
	}
	res, err := in.Rerun(context.Background(), first.Blob.ID, "прочитай текст")
	if err != nil {
		t.Fatalf("rerun: %v", err)
	}
	if res.Description != "текст: ошибка E24" {
		t.Errorf("description = %q", res.Description)
	}
	if fp.lastQ != "прочитай текст" {
		t.Errorf("new question not used: %q", fp.lastQ)
	}
	if fp.seen != 2 {
		t.Errorf("provider called %d times, want 2", fp.seen)
	}
}

func TestRerunRefusesAudioBlob(t *testing.T) {
	in, store := testIntake(t, &fakeProvider{reply: "x"})
	b, err := store.Put(media.KindAudio, "audio/wav", "capture:meeting", []byte("pcm bytes"))
	if err != nil {
		t.Fatal(err)
	}
	if _, err := in.Rerun(context.Background(), b.ID, ""); err == nil {
		t.Error("audio blob was accepted as an image")
	}
}

func TestRerunUnknownID(t *testing.T) {
	in, _ := testIntake(t, &fakeProvider{})
	if _, err := in.Rerun(context.Background(), "nope", ""); err == nil {
		t.Error("malformed id accepted")
	}
}

func TestAcceptRefusesNonImage(t *testing.T) {
	in, _ := testIntake(t, &fakeProvider{})
	if _, err := in.Accept(context.Background(), []byte("this is a text file"), "web:upload", ""); !errors.Is(err, media.ErrUnsupportedImage) {
		t.Errorf("got %v, want ErrUnsupportedImage", err)
	}
}

func TestNilProviderDegradesToDisabled(t *testing.T) {
	s, err := media.Open(t.TempDir(), 0, 0)
	if err != nil {
		t.Fatal(err)
	}
	in := NewIntake(s, nil, 0)
	if _, err := in.Accept(context.Background(), pngPayload(t, 8, 8), "x", ""); !errors.Is(err, ErrDisabled) {
		t.Errorf("got %v, want ErrDisabled", err)
	}
}
