package projectmemory

import (
	"context"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/infra/llm"
)

// fakeRepo is an in-memory Repository for service tests.
type fakeRepo struct {
	entries     map[uuid.UUID]*Entry
	embedding   map[uuid.UUID][]float32
	knnResult   []Entry // returned by SearchKNN
	keywordHits []Entry // returned by SearchKeyword
	knnCalls    int
	kwCalls     int
}

func newFakeRepo() *fakeRepo {
	return &fakeRepo{entries: map[uuid.UUID]*Entry{}, embedding: map[uuid.UUID][]float32{}}
}

func (r *fakeRepo) Insert(_ context.Context, in WriteInput, model *string) (*Entry, error) {
	e := &Entry{
		ID: uuid.New(), ProjectID: in.ProjectID, WorkspaceID: in.WorkspaceID,
		Kind: in.Kind, Title: in.Title, Body: in.Body, Tags: in.Tags, Source: in.Source,
		EmbeddingModel: model, CreatedBy: in.CreatedBy, CreatedAt: time.Now(), UpdatedAt: time.Now(),
	}
	if e.Tags == nil {
		e.Tags = []string{}
	}
	r.entries[e.ID] = e
	return e, nil
}
func (r *fakeRepo) SetEmbedding(_ context.Context, id uuid.UUID, vec []float32, _ string) error {
	r.embedding[id] = vec
	return nil
}
func (r *fakeRepo) Get(_ context.Context, id uuid.UUID) (*Entry, error) { return r.entries[id], nil }
func (r *fakeRepo) ListByProject(_ context.Context, _ uuid.UUID, _ string) ([]Entry, error) {
	out := make([]Entry, 0, len(r.entries))
	for _, e := range r.entries {
		out = append(out, *e)
	}
	return out, nil
}
func (r *fakeRepo) SearchKeyword(_ context.Context, _ uuid.UUID, _, _ string, _ int) ([]Entry, error) {
	r.kwCalls++
	return r.keywordHits, nil
}
func (r *fakeRepo) SearchKNN(_ context.Context, _ uuid.UUID, _ []float32, _ string, _ int) ([]Entry, error) {
	r.knnCalls++
	return r.knnResult, nil
}
func (r *fakeRepo) Delete(_ context.Context, id uuid.UUID) error { delete(r.entries, id); return nil }

type fakeSel struct{ emb llm.Embedder }

func (s fakeSel) Embedder(context.Context) (llm.Embedder, uuid.UUID, string, int, error) {
	if s.emb == nil {
		return nil, uuid.Nil, "", 0, llm.ErrEmbeddingsUnsupported
	}
	return s.emb, uuid.New(), "fake-model", llm.PlatformEmbeddingDims, nil
}

func TestService_Write_WithEmbedder(t *testing.T) {
	repo := newFakeRepo()
	svc := NewService(repo, fakeSel{emb: llm.NewFakeEmbedder()}, nil)
	e, err := svc.Write(context.Background(), WriteInput{
		ProjectID: uuid.New(), Kind: KindDecision, Title: "t", Body: "b",
	})
	require.NoError(t, err)
	require.NotNil(t, e.EmbeddingModel)
	require.NotEmpty(t, repo.embedding[e.ID], "embedding must be stored")
}

func TestService_Write_WithoutEmbedder(t *testing.T) {
	repo := newFakeRepo()
	svc := NewService(repo, fakeSel{emb: nil}, nil) // disabled
	e, err := svc.Write(context.Background(), WriteInput{
		ProjectID: uuid.New(), Kind: KindGotcha, Title: "t", Body: "b",
	})
	require.NoError(t, err)
	require.Nil(t, e.EmbeddingModel)
	require.Empty(t, repo.embedding[e.ID])
}

func TestService_Write_InvalidKind(t *testing.T) {
	svc := NewService(newFakeRepo(), fakeSel{}, nil)
	_, err := svc.Write(context.Background(), WriteInput{ProjectID: uuid.New(), Kind: "bogus", Title: "t", Body: "b"})
	require.Error(t, err)
}

func TestService_Search_VectorPath(t *testing.T) {
	repo := newFakeRepo()
	repo.knnResult = []Entry{{ID: uuid.New(), Kind: KindDecision, Title: "hit"}}
	svc := NewService(repo, fakeSel{emb: llm.NewFakeEmbedder()}, nil)
	res, err := svc.Search(context.Background(), Query{ProjectID: uuid.New(), Text: "q"})
	require.NoError(t, err)
	require.Len(t, res, 1)
	require.Equal(t, 1, repo.knnCalls)
	require.Equal(t, 0, repo.kwCalls)
}

func TestService_Search_KeywordFallback_NoEmbedder(t *testing.T) {
	repo := newFakeRepo()
	repo.keywordHits = []Entry{{ID: uuid.New(), Kind: KindDecision, Title: "kw"}}
	svc := NewService(repo, fakeSel{emb: nil}, nil) // disabled → keyword path
	res, err := svc.Search(context.Background(), Query{ProjectID: uuid.New(), Text: "q"})
	require.NoError(t, err)
	require.Len(t, res, 1)
	require.Equal(t, 0, repo.knnCalls)
	require.Equal(t, 1, repo.kwCalls)
}

func TestService_Search_VectorEmptyFallsBackToKeyword(t *testing.T) {
	repo := newFakeRepo()
	repo.knnResult = nil // no embedded entries yet
	repo.keywordHits = []Entry{{ID: uuid.New(), Title: "kw"}}
	svc := NewService(repo, fakeSel{emb: llm.NewFakeEmbedder()}, nil)
	res, err := svc.Search(context.Background(), Query{ProjectID: uuid.New(), Text: "q"})
	require.NoError(t, err)
	require.Len(t, res, 1)
	require.Equal(t, 1, repo.knnCalls)
	require.Equal(t, 1, repo.kwCalls)
}

var _ Repository = (*fakeRepo)(nil)
