package backend

import (
	"context"
	"errors"
	"fmt"
	"math"
	"strings"
	"testing"
	"time"

	"github.com/c4pt0r/agfs/agfs-server/pkg/filesystem"
	"go.uber.org/zap"
	"go.uber.org/zap/zaptest/observer"

	"github.com/mem9-ai/drive9/pkg/logger"
)

func TestValidateAppEmbeddingVector(t *testing.T) {
	valid := make([]float32, 1024)
	if err := validateAppEmbeddingVector(valid); err != nil {
		t.Fatalf("valid vector rejected: %v", err)
	}
	if err := validateAppEmbeddingVector(make([]float32, 1023)); err == nil {
		t.Fatal("wrong-dimension vector accepted")
	}
	nonFinite := make([]float32, 1024)
	nonFinite[17] = float32(math.Inf(1))
	if err := validateAppEmbeddingVector(nonFinite); err == nil {
		t.Fatal("non-finite vector accepted")
	}
}

type staticQueryEmbedder struct {
	vec []float32
	err error
}

func (e staticQueryEmbedder) EmbedText(context.Context, string) ([]float32, error) {
	if e.err != nil {
		return nil, e.err
	}
	return append([]float32(nil), e.vec...), nil
}

type recordingQueryEmbedder struct {
	seen chan string
	err  error
}

func (e recordingQueryEmbedder) EmbedText(_ context.Context, text string) ([]float32, error) {
	select {
	case e.seen <- text:
	default:
	}
	return nil, e.err
}

func TestGrepFallsBackToKeywordWithoutEmbedder(t *testing.T) {
	b := newTestBackend(t)
	if _, err := b.Write("/notes/a.txt", []byte("hello keyword fallback"), 0, filesystem.WriteFlagCreate); err != nil {
		t.Fatal(err)
	}

	results, err := b.Grep(context.Background(), "keyword", "/notes/", 20)
	if err != nil {
		t.Fatal(err)
	}
	if len(results) != 1 || results[0].Path != "/notes/a.txt" {
		t.Fatalf("unexpected grep results: %+v", results)
	}
}

func TestGrepSkipsDefaultEmbedderWhenAppEmbeddingDisabled(t *testing.T) {
	previousLogger := logger.L()
	core, recorded := observer.New(zap.WarnLevel)
	logger.Set(zap.New(core))
	t.Cleanup(func() { logger.Set(previousLogger) })

	b := newTestBackend(t)
	if _, err := b.Write("/notes/default-disabled.txt", []byte("keyword only search"), 0, filesystem.WriteFlagCreate); err != nil {
		t.Fatal(err)
	}
	results, err := b.Grep(context.Background(), "keyword", "/notes/", 20)
	if err != nil {
		t.Fatal(err)
	}
	if len(results) != 1 || results[0].Path != "/notes/default-disabled.txt" {
		t.Fatalf("unexpected grep results: %+v", results)
	}
	if entries := recorded.FilterMessage("backend_grep_query_embedding_failed").All(); len(entries) != 0 {
		t.Fatalf("disabled app embedding logged %d query provider failures, want 0", len(entries))
	}
}

func TestGrepFallsBackToKeywordWhenEmbeddingUnavailable(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{
		AppSemanticTasksEnabled:    true,
		AppEmbeddingGeneration:     1,
		AppEmbeddingConfigRevision: 1,
		QueryEmbedding: QueryEmbeddingOptions{
			Client: staticQueryEmbedder{err: errors.New("embed unavailable")},
		},
	})
	if _, err := b.Write("/notes/b.txt", []byte("hello semantic fallback"), 0, filesystem.WriteFlagCreate); err != nil {
		t.Fatal(err)
	}

	results, err := b.Grep(context.Background(), "semantic", "/notes/", 20)
	if err != nil {
		t.Fatal(err)
	}
	if len(results) != 1 || results[0].Path != "/notes/b.txt" {
		t.Fatalf("unexpected grep results: %+v", results)
	}
}

func TestGrepDoesNotLogQueryEmbeddingProviderDetails(t *testing.T) {
	const providerDetail = "provider-secret-response-detail"
	previousLogger := logger.L()
	core, recorded := observer.New(zap.WarnLevel)
	logger.Set(zap.New(core))
	t.Cleanup(func() { logger.Set(previousLogger) })

	b := newTestBackendWithOptions(t, Options{
		AppSemanticTasksEnabled:    true,
		AppEmbeddingGeneration:     1,
		AppEmbeddingConfigRevision: 1,
		QueryEmbedding: QueryEmbeddingOptions{
			Client: staticQueryEmbedder{err: errors.New(providerDetail)},
		},
	})
	if _, err := b.Write("/notes/sanitized.txt", []byte("safe keyword fallback"), 0, filesystem.WriteFlagCreate); err != nil {
		t.Fatal(err)
	}
	if _, err := b.Grep(context.Background(), "keyword", "/notes/", 20); err != nil {
		t.Fatal(err)
	}

	entries := recorded.FilterMessage("backend_grep_query_embedding_failed").All()
	if len(entries) != 1 {
		t.Fatalf("query embedding failure logs = %d, want 1", len(entries))
	}
	if strings.Contains(entries[0].Message+fmt.Sprint(entries[0].ContextMap()), providerDetail) {
		t.Fatalf("provider detail leaked into log: %q", providerDetail)
	}
	if got := entries[0].ContextMap()["failure_category"]; got != "request_failed" {
		t.Fatalf("failure_category = %v, want request_failed", got)
	}
}

func TestGrepEmbedsQueryInApplicationLayerBeforeFallback(t *testing.T) {
	seen := make(chan string, 1)
	b := newTestBackendWithOptions(t, Options{
		AppSemanticTasksEnabled:    true,
		AppEmbeddingGeneration:     1,
		AppEmbeddingConfigRevision: 1,
		QueryEmbedding: QueryEmbeddingOptions{
			Client: recordingQueryEmbedder{seen: seen, err: errors.New("embed unavailable")},
		},
	})
	if _, err := b.Write("/notes/query.txt", []byte("semantic probe from app side embed"), 0, filesystem.WriteFlagCreate); err != nil {
		t.Fatal(err)
	}

	results, err := b.Grep(context.Background(), "semantic probe", "/notes/", 20)
	if err != nil {
		t.Fatal(err)
	}
	if len(results) != 1 || results[0].Path != "/notes/query.txt" {
		t.Fatalf("unexpected grep results: %+v", results)
	}

	select {
	case got := <-seen:
		if got != "semantic probe" {
			t.Fatalf("query embedder saw %q, want %q", got, "semantic probe")
		}
	case <-time.After(time.Second):
		t.Fatal("query embedder was not called")
	}
}

func TestGrepUsesFTSResultsWhenVectorQueryFails(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{
		AppSemanticTasksEnabled:    true,
		AppEmbeddingGeneration:     1,
		AppEmbeddingConfigRevision: 1,
		QueryEmbedding: QueryEmbeddingOptions{
			Client: staticQueryEmbedder{vec: []float32{0.1, 0.2, 0.3}},
		},
	})
	if _, err := b.Write("/notes/c.txt", []byte("hello vector fallback"), 0, filesystem.WriteFlagCreate); err != nil {
		t.Fatal(err)
	}

	results, err := b.Grep(context.Background(), "vector", "/notes/", 20)
	if err != nil {
		t.Fatal(err)
	}
	if len(results) != 1 || results[0].Path != "/notes/c.txt" {
		t.Fatalf("unexpected grep results: %+v", results)
	}
}

func TestGrepFallsBackToKeywordWhenFTSIsEmptyAndVectorFails(t *testing.T) {
	b := newTestBackendWithOptions(t, Options{
		AppSemanticTasksEnabled:    true,
		AppEmbeddingGeneration:     1,
		AppEmbeddingConfigRevision: 1,
		QueryEmbedding: QueryEmbeddingOptions{
			Client: staticQueryEmbedder{err: errors.New("vector unavailable")},
		},
	})
	if _, err := b.Write("/notes/d.txt", []byte("connection pooling keeps latency stable"), 0, filesystem.WriteFlagCreate); err != nil {
		t.Fatal(err)
	}

	results, err := b.Grep(context.Background(), "pool", "/notes/", 20)
	if err != nil {
		t.Fatal(err)
	}
	if len(results) != 1 || results[0].Path != "/notes/d.txt" {
		t.Fatalf("unexpected grep results: %+v", results)
	}
}

func TestGrepContentTextAlwaysWrittenToSemantic(t *testing.T) {
	// Regression: content_text must always be written to the semantic table
	// so FTS and LIKE searches work even when auto-embedding is disabled.
	b := newTestBackend(t)
	if _, err := b.Write("/notes/always.txt", []byte("content always indexed"), 0, filesystem.WriteFlagCreate); err != nil {
		t.Fatal(err)
	}

	results, err := b.Grep(context.Background(), "always indexed", "/notes/", 20)
	if err != nil {
		t.Fatal(err)
	}
	if len(results) != 1 || results[0].Path != "/notes/always.txt" {
		t.Fatalf("expected grep to find /notes/always.txt via keyword, got %+v", results)
	}
}

func TestGrepContentTextWrittenOnOverwrite(t *testing.T) {
	// Verify content_text is updated when a file is overwritten.
	b := newTestBackend(t)
	if _, err := b.Write("/notes/overwrite.txt", []byte("original content alpha"), 0, filesystem.WriteFlagCreate); err != nil {
		t.Fatal(err)
	}
	if _, err := b.Write("/notes/overwrite.txt", []byte("replaced content beta"), 0, filesystem.WriteFlagTruncate); err != nil {
		t.Fatal(err)
	}

	// Old content should not match.
	results, err := b.Grep(context.Background(), "alpha", "/notes/", 20)
	if err != nil {
		t.Fatal(err)
	}
	if len(results) != 0 {
		t.Fatalf("expected old content 'alpha' to not match after overwrite, got %+v", results)
	}

	// New content should match.
	results, err = b.Grep(context.Background(), "beta", "/notes/", 20)
	if err != nil {
		t.Fatal(err)
	}
	if len(results) != 1 || results[0].Path != "/notes/overwrite.txt" {
		t.Fatalf("expected grep to find /notes/overwrite.txt via keyword 'beta', got %+v", results)
	}
}

func TestGrepAutoEmbeddingSkipsApplicationQueryEmbedder(t *testing.T) {
	seen := make(chan string, 1)
	b := newTestBackendWithOptions(t, Options{
		DatabaseAutoEmbedding: true,
		QueryEmbedding: QueryEmbeddingOptions{
			Client: recordingQueryEmbedder{seen: seen, err: errors.New("should not be called")},
		},
	})
	if _, err := b.Write("/notes/auto.txt", []byte("semantic probe from database side"), 0, filesystem.WriteFlagCreate); err != nil {
		t.Fatal(err)
	}

	results, err := b.Grep(context.Background(), "semantic probe", "/notes/", 20)
	if err != nil {
		t.Fatal(err)
	}
	if len(results) != 1 || results[0].Path != "/notes/auto.txt" {
		t.Fatalf("unexpected grep results: %+v", results)
	}

	select {
	case got := <-seen:
		t.Fatalf("query embedder was called in auto mode with %q", got)
	default:
	}
}
