package vectord

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync"
	"testing"

	"git.agentview.dev/profit/golangLAKEHOUSE/internal/storeclient"
)

// memStore is an in-memory Store fake for unit tests. Returns the
// real storeclient.ErrKeyNotFound sentinel so persistor.Load's
// errors.Is path is exercised faithfully.
type memStore struct {
	mu   sync.Mutex
	data map[string][]byte
}

func newMemStore() *memStore { return &memStore{data: map[string][]byte{}} }

func (m *memStore) Put(_ context.Context, key string, body []byte) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	cp := make([]byte, len(body))
	copy(cp, body)
	m.data[key] = cp
	return nil
}

func (m *memStore) Get(_ context.Context, key string) ([]byte, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	b, ok := m.data[key]
	if !ok {
		return nil, storeclient.ErrKeyNotFound
	}
	return b, nil
}

func (m *memStore) Delete(_ context.Context, key string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	delete(m.data, key)
	return nil
}

func (m *memStore) List(_ context.Context, prefix string) ([]string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	out := []string{}
	for k := range m.data {
		if strings.HasPrefix(k, prefix) {
			out = append(out, k)
		}
	}
	return out, nil
}

func TestPersistor_SaveLoad_RoundTrip(t *testing.T) {
	const n = 16
	store := newMemStore()
	p := NewPersistor(store)

	src, _ := NewIndex(IndexParams{Name: "workers", Dimension: n, Distance: DistanceCosine})
	for i := 0; i < n; i++ {
		v := make([]float32, n)
		v[i%n] = 1.0
		v[(i+1)%n] = 0.001
		_ = src.Add(fmt.Sprintf("w-%02d", i), v, json.RawMessage(fmt.Sprintf(`{"i":%d}`, i)))
	}
	if err := p.Save(context.Background(), src); err != nil {
		t.Fatal(err)
	}

	dst, err := p.Load(context.Background(), "workers")
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if dst.Len() != src.Len() {
		t.Errorf("Len: src=%d dst=%d", src.Len(), dst.Len())
	}
	for i := 0; i < n; i++ {
		v := make([]float32, n)
		v[i%n] = 1.0
		v[(i+1)%n] = 0.001
		hits, _ := dst.Search(v, 1)
		want := fmt.Sprintf("w-%02d", i)
		if len(hits) == 0 || hits[0].ID != want {
			t.Errorf("recall after Load: i=%d hits=%v", i, hits)
		}
	}
}

func TestPersistor_Load_MissingReturnsErrKeyMissing(t *testing.T) {
	p := NewPersistor(newMemStore())
	_, err := p.Load(context.Background(), "nope")
	if !errors.Is(err, ErrKeyMissing) {
		t.Errorf("expected ErrKeyMissing, got %v", err)
	}
}

func TestPersistor_Delete_RemovesBothFiles(t *testing.T) {
	store := newMemStore()
	p := NewPersistor(store)
	src, _ := NewIndex(IndexParams{Name: "x", Dimension: 4})
	_ = src.Add("a", []float32{1, 0, 0, 0}, nil)
	_ = p.Save(context.Background(), src)

	if err := p.Delete(context.Background(), "x"); err != nil {
		t.Fatal(err)
	}
	if _, err := p.Load(context.Background(), "x"); !errors.Is(err, ErrKeyMissing) {
		t.Errorf("after Delete, Load should ErrKeyMissing, got %v", err)
	}
}

func TestPersistor_List_FiltersBySuffix(t *testing.T) {
	// Single-file format means no orphan-pair concept; we just
	// filter on the .lhv1 suffix. Garbage files under VectorPrefix
	// (e.g. operator drops something there) shouldn't show up.
	store := newMemStore()
	p := NewPersistor(store)

	src, _ := NewIndex(IndexParams{Name: "alpha", Dimension: 4})
	_ = src.Add("a", []float32{1, 0, 0, 0}, nil)
	_ = p.Save(context.Background(), src)

	src2, _ := NewIndex(IndexParams{Name: "beta", Dimension: 4})
	_ = src2.Add("b", []float32{0, 1, 0, 0}, nil)
	_ = p.Save(context.Background(), src2)

	// A garbage file under the prefix that shouldn't match.
	_ = store.Put(context.Background(), VectorPrefix+"README.txt", []byte("not an index"))

	got, err := p.List(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 2 || got[0] != "alpha" || got[1] != "beta" {
		t.Errorf("List: got %v, want [alpha beta]", got)
	}
}

func TestPersistor_Load_BadFormat(t *testing.T) {
	store := newMemStore()
	p := NewPersistor(store)
	// Manually plant a file with bad magic → Load surfaces ErrBadFormat.
	_ = store.Put(context.Background(), fileKey("corrupt"), []byte("not lhv1 framing"))
	_, err := p.Load(context.Background(), "corrupt")
	if !errors.Is(err, ErrBadFormat) {
		t.Errorf("expected ErrBadFormat, got %v", err)
	}
}
