package store

import (
	"context"
	"path/filepath"
	"strings"
	"testing"

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

func newMemTestStore(t *testing.T) *Store {
	t.Helper()
	path := filepath.Join(t.TempDir(), "mem_test.db")
	st, err := Open(context.Background(), path)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	t.Cleanup(func() { _ = st.Close() })
	return st
}

func TestMemoryStoreInsertSearch(t *testing.T) {
	ctx := context.Background()
	m := newMemTestStore(t).VectorMemory()

	// three orthonormal-ish vectors; a query aligned with the second must rank it top.
	if err := m.Insert(ctx, "a", []float32{1, 0, 0}, map[string]string{"text": "вода"}); err != nil {
		t.Fatal(err)
	}
	if err := m.Insert(ctx, "b", []float32{0, 1, 0}, map[string]string{"text": "сон", "type": "fact"}); err != nil {
		t.Fatal(err)
	}
	if err := m.Insert(ctx, "c", []float32{0, 0, 1}, map[string]string{"text": "еда"}); err != nil {
		t.Fatal(err)
	}

	got, err := m.Search(ctx, []float32{0, 1, 0}, 2)
	if err != nil {
		t.Fatalf("Search: %v", err)
	}
	if len(got) != 2 {
		t.Fatalf("topK=2 returned %d results", len(got))
	}
	if got[0].ID != "b" {
		t.Errorf("top hit = %q, want b", got[0].ID)
	}
	if got[0].Meta["text"] != "сон" || got[0].Meta["type"] != "fact" {
		t.Errorf("meta not round-tripped: %v", got[0].Meta)
	}
	if got[0].Score < 0.99 {
		t.Errorf("aligned vector score = %v, want ~1.0", got[0].Score)
	}
}

func TestMemoryStoreUpsertReplaces(t *testing.T) {
	ctx := context.Background()
	m := newMemTestStore(t).VectorMemory()

	if err := m.Insert(ctx, "x", []float32{1, 0}, map[string]string{"text": "старое"}); err != nil {
		t.Fatal(err)
	}
	if err := m.Insert(ctx, "x", []float32{0, 1}, map[string]string{"text": "новое"}); err != nil {
		t.Fatal(err)
	}
	got, err := m.Search(ctx, []float32{0, 1}, 10)
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 1 {
		t.Fatalf("re-inserting the same id produced %d rows, want 1 (upsert)", len(got))
	}
	if got[0].Meta["text"] != "новое" {
		t.Errorf("upsert kept the old value: %q", got[0].Meta["text"])
	}
}

func TestMemoryStorePersistsAcrossReopen(t *testing.T) {
	ctx := context.Background()
	path := filepath.Join(t.TempDir(), "persist.db")

	st, err := Open(ctx, path)
	if err != nil {
		t.Fatal(err)
	}
	if err := st.VectorMemory().Insert(ctx, "k", []float32{1, 0, 0}, map[string]string{"text": "запомни"}); err != nil {
		t.Fatal(err)
	}
	if err := st.Close(); err != nil {
		t.Fatal(err)
	}

	// reopen the same file — the in-memory floor would have lost this.
	st2, err := Open(ctx, path)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = st2.Close() })
	got, err := st2.VectorMemory().Search(ctx, []float32{1, 0, 0}, 1)
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 1 || got[0].Meta["text"] != "запомни" {
		t.Fatalf("memory did not survive reopen: %v", got)
	}
}

// A voiceprint sharing the vector table must never rank as a note hit. The
// dimensions match here on purpose: what used to hide these rows was cosine
// returning 0 on a width mismatch, which is a property of two model choices and
// not of the store.
func TestMemoryStoreSearchSkipsVoiceprints(t *testing.T) {
	ctx := context.Background()
	m := newMemTestStore(t).VectorMemory()

	if err := m.Insert(ctx, "note:1", []float32{0, 1, 0}, map[string]string{"text": "заметка"}); err != nil {
		t.Fatal(err)
	}
	if err := m.Insert(ctx, memory.NonRecallPrefix+"kami", []float32{1, 0, 0}, map[string]string{"name": "Ками"}); err != nil {
		t.Fatal(err)
	}

	got, err := m.Search(ctx, []float32{1, 0, 0}, 10)
	if err != nil {
		t.Fatalf("Search: %v", err)
	}
	for _, r := range got {
		if strings.HasPrefix(r.ID, memory.NonRecallPrefix) {
			t.Fatalf("recall returned a voiceprint: %+v", r)
		}
	}
	if len(got) != 1 || got[0].ID != "note:1" {
		t.Fatalf("Search = %+v, want just the note", got)
	}

	// The row is still there for the speaker code that owns it.
	recs, err := m.ByPrefix(ctx, memory.NonRecallPrefix)
	if err != nil || len(recs) != 1 {
		t.Fatalf("ByPrefix = %+v, %v; want the voiceprint", recs, err)
	}
}
