package services

import (
	"context"
	"encoding/json"
	"math"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"wa-assistant/backend/models"
)

// Opt-in retrieval acceptance with real OpenRouter vectors and reviewed local
// source IDs. Production knowledge is read only; all test state lives in SQLite.
func TestLiveOpenRouterKnowledgeRetrieval(t *testing.T) {
	if os.Getenv("CHATLOOP_RUN_LIVE_EMBEDDING") != "1" {
		t.Skip("opt-in paid OpenRouter embedding acceptance")
	}
	_, agent, knowledge, _ := liveDeepSeekAssistantSnapshot(t)
	db := agenticTestDB(t)
	if err := db.AutoMigrate(&models.AppSetting{}, &models.Agent{}); err != nil {
		t.Fatal(err)
	}
	if len(knowledge) == 0 {
		t.Fatal("No published knowledge")
	}
	if err := db.Create(&knowledge).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&models.AppSetting{Key: "embedding_model", Value: os.Getenv("CHATLOOP_LIVE_EMBEDDING_MODEL")}).Error; err != nil {
		t.Fatal(err)
	}
	embMu.Lock()
	oldClient, oldModel, oldDims, oldEnabled := embClient, embModel, embDims, embEnabled
	embMu.Unlock()
	t.Cleanup(func() {
		embMu.Lock()
		embClient, embModel, embDims, embEnabled = oldClient, oldModel, oldDims, oldEnabled
		embMu.Unlock()
		InvalidateKB(agent.ID)
	})
	InitEmbedding()
	InvalidateKB(agent.ID)
	state, err := KnowledgeIndexStatus(context.Background(), agent.ID)
	if err != nil || !state.Configured || state.Embedded != state.Available || state.Available == 0 {
		t.Fatal("Real embedding configuration and a complete current index are required")
	}
	items, err := LoadKnowledgeForContext(context.Background(), agent.ID)
	if err != nil {
		t.Fatal(err)
	}
	for _, item := range items {
		if item.K.ID == 39 || item.K.ID == 48 {
			t.Fatal("Archived licensing knowledge entered retrieval")
		}
		for _, value := range item.Vec {
			if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {
				t.Fatal("Index contains a non-finite vector")
			}
		}
	}
	t.Logf("model=%s indexed=%d/%d vector_dimensions=%d", embSignature(), state.Embedded, state.Available, len(items[0].Vec))
	type retrievalResult struct {
		Name        string  `json:"name"`
		Question    string  `json:"question"`
		ExpectedIDs []uint  `json:"expected_ids"`
		RequireAll  bool    `json:"require_all"`
		Retrieved   []uint  `json:"retrieved"`
		Mode        string  `json:"mode"`
		Similarity  float64 `json:"similarity"`
		DurationMS  int64   `json:"duration_ms"`
		Passed      bool    `json:"passed"`
	}
	results := []retrievalResult{}
	for _, tc := range []struct {
		name, question string
		ids            []uint
		all            bool
	}{
		{"scheduled_messages", "WA Blast: bisa menyiapkan pesan sekarang supaya terkirim besok pagi?", []uint{33, 43}, false},
		{"automatic_followup", "WA Blast: setelah calon pembeli tertarik, bisa disapa lagi otomatis?", []uint{35}, false},
		{"customer_contacts", "WA Blast: saya ingin menyimpan daftar pelanggan sekaligus mengelompokkannya", []uint{36}, false},
		{"blocking_risk", "WA Blast: kalau nomor saya diblokir, apakah ada jaminan bebas risiko?", []uint{46}, false},
		{"licensed_client_work", "WA Blast: jasa pasang untuk pelanggan lain yang masing-masing punya lisensi sendiri", []uint{100, 101}, false},
		{"rental_permission", "WA Blast: saya mau menyewakan akses aplikasinya setiap bulan ke orang lain", []uint{100, 101}, false},
		{"future_learning_access", "Masterclass Laravel 12: kalau belinya sekarang, tahun depan masih boleh mengulang pelajarannya?", []uint{72, 84}, false},
		{"one_time_course_payment", "Masterclass Laravel 12: duitnya dibayar satu kali atau ditagih tiap bulan?", []uint{70}, false},
		{"blast_price", "harga source code WA Blast plus AI Assistant", []uint{28}, false},
		{"compare_prices", "Bandingkan harga kelas Laravel 12 dan source code WA Blast plus AI Assistant. Buat satu poin per produk.", []uint{28, 70}, true},
	} {
		t.Run(tc.name, func(t *testing.T) {
			ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
			defer cancel()
			start := time.Now()
			rows, mode, similarity := selectKnowledgeWithLimit(ctx, tc.question, items, 5)
			got := idsOf(rows)
			matches := 0
			for _, id := range got {
				for _, expected := range tc.ids {
					if id == expected {
						matches++
					}
				}
			}
			semantic := mode == "hybrid" || mode == "semantic"
			found := matches > 0 && (!tc.all || matches == len(tc.ids))
			passed := found && semantic && similarity > 0 && ctx.Err() == nil
			results = append(results, retrievalResult{Name: tc.name, Question: tc.question, ExpectedIDs: tc.ids, RequireAll: tc.all, Retrieved: got, Mode: mode, Similarity: similarity, DurationMS: time.Since(start).Milliseconds(), Passed: passed})
			t.Logf("mode=%s similarity=%.3f sources=%v duration=%s", mode, similarity, got, time.Since(start).Round(time.Millisecond))
			if !passed {
				t.Errorf("Expected current source %v with a working semantic signal, got %v (%s)", tc.ids, got, mode)
			}
			for _, row := range rows {
				if row.AgentID != agent.ID || !row.Active || row.ReviewStatus != "published" || strings.Contains(mode, "conflict") {
					t.Error("Invalid retrieval scope or unresolved policy conflict")
				}
			}
		})
	}
	if output := os.Getenv("CHATLOOP_EMBEDDING_REPORT"); output != "" {
		raw, err := json.MarshalIndent(map[string]any{"model": embSignature(), "indexed": state.Embedded, "available": state.Available, "dimensions": len(items[0].Vec), "results": results}, "", "  ")
		if err != nil {
			t.Fatal(err)
		}
		if err := os.MkdirAll(filepath.Dir(output), 0700); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(output, raw, 0600); err != nil {
			t.Fatal(err)
		}
	}
}
