package chunk

import (
	"bytes"
	"context"
	"errors"
	"image"
	"image/color"
	"image/png"
	"ragflow/internal/common"
	"ragflow/internal/dao"
	"ragflow/internal/engine/types"
	"ragflow/internal/entity"
	"ragflow/internal/entity/models"
	"ragflow/internal/service"
	"ragflow/internal/service/document"
	"ragflow/internal/storage"
	"reflect"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
)

func TestIsZeroVector(t *testing.T) {
	if !common.IsZeroVector([]float64{0, 0, 0}) {
		t.Error("all zeros should be true")
	}
	if common.IsZeroVector([]float64{0, 1, 0}) {
		t.Error("non-zero should be false")
	}
	if !common.IsZeroVector([]float64{}) {
		t.Error("empty should be true (treated as zero)")
	}
	if !common.IsZeroVector(nil) {
		t.Error("nil should be true")
	}
}

func TestHydrateChunkVectors_AllNonZero(t *testing.T) {
	chunks := []map[string]interface{}{
		{"id": "c1", "vector": []float64{1, 2, 3}},
		{"id": "c2", "vector": []float64{4, 5, 6}},
	}
	// No zero vectors → nothing to hydrate.
	hydrateChunkVectors(context.Background(), nil, chunks, nil, nil)
	if !reflect.DeepEqual(chunks[0]["vector"], []float64{1, 2, 3}) {
		t.Error("non-zero vector should not be changed")
	}
	if !reflect.DeepEqual(chunks[1]["vector"], []float64{4, 5, 6}) {
		t.Error("non-zero vector should not be changed")
	}
}

func TestHydrateChunkVectors_EmptyChunks(t *testing.T) {
	// Should not panic on empty or nil.
	hydrateChunkVectors(context.Background(), nil, nil, nil, nil)
	hydrateChunkVectors(context.Background(), nil, []map[string]interface{}{}, nil, nil)
}

func TestHydrateChunkVectors_MissingIDs(t *testing.T) {
	chunks := []map[string]interface{}{
		{"vector": []float64{1.0}}, // no id — skipped
	}
	hydrateChunkVectors(context.Background(), nil, chunks, nil, nil)
	// Should not change anything when engine is nil (FetchChunkVectors returns zero vectors).
	// The function doesn't panic — it just can't hydrate because dim is 0.
	// With nil engine, FetchChunkVectors returns zero vectors, so the zero stays zero.
}

func TestHydrateChunkVectors_NoDim(t *testing.T) {
	chunks := []map[string]interface{}{
		{"id": "c1", "vector": []float64{}},
	}
	hydrateChunkVectors(context.Background(), nil, chunks, []string{"kb1"}, []string{"t1"})
	// Empty vectors have dim=0 → early return. No crash.
}

func TestKnowledgebaseEmbeddingKey(t *testing.T) {
	tenantEmbdID := "42"

	tests := []struct {
		name     string
		kb       *entity.Knowledgebase
		tenantID string
		want     string
	}{
		{
			name: "uses tenant embedding id before embd id",
			kb: &entity.Knowledgebase{
				EmbdID:       "shared-model",
				TenantEmbdID: &tenantEmbdID,
			},
			want: "tenant:42",
		},
		{
			name: "uses embd id without tenant embedding id",
			kb: &entity.Knowledgebase{
				EmbdID: "shared-model",
			},
			want: "embd:shared-model",
		},
		{
			name:     "uses tenant default when embedding id is empty",
			kb:       &entity.Knowledgebase{},
			tenantID: "tenant-1",
			want:     "default:tenant-1",
		},
		{
			name: "ignores non-positive tenant embedding id",
			kb: &entity.Knowledgebase{
				EmbdID:       "shared-model",
				TenantEmbdID: func() *string { s := ""; return &s }(),
			},
			want: "embd:shared-model",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := knowledgebaseEmbeddingKey(tt.kb, tt.tenantID); got != tt.want {
				t.Fatalf("knowledgebaseEmbeddingKey() = %q, want %q", got, tt.want)
			}
		})
	}
}

func TestParsePrevalidatesDocumentsBeforeMutating(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)

	userID := "user-1"
	datasetID := "kb-1"
	insertChunkTestKB(t, datasetID, userID)
	insertChunkTestKB(t, "kb-2", userID)
	insertChunkTestDoc(t, "doc-1", datasetID)
	insertChunkTestDoc(t, "doc-2", "kb-2")
	insertChunkTestTask(t, "task-1", "doc-1")

	svc := &ChunkService{
		docEngine:   nil,
		kbDAO:       dao.NewKnowledgebaseDAO(),
		documentDAO: dao.NewDocumentDAO(),
	}

	ctx := t.Context()
	_, code, err := svc.Parse(ctx, userID, datasetID, &service.ParseFileRequest{
		DocumentIDs: []string{"doc-1", "missing-doc", "doc-2"},
	})
	if err == nil {
		t.Fatal("expected parse to fail")
	}
	if code != common.CodeDataError {
		t.Fatalf("expected CodeDataError, got %v", code)
	}
	if !strings.Contains(err.Error(), "missing-doc") || !strings.Contains(err.Error(), "doc-2") {
		t.Fatalf("expected missing and foreign documents in error, got %q", err.Error())
	}

	var taskCount int64
	if err = db.Model(&entity.Task{}).Where("doc_id = ?", "doc-1").Count(&taskCount).Error; err != nil {
		t.Fatalf("count tasks: %v", err)
	}
	if taskCount != 1 {
		t.Fatalf("expected existing task to remain, got %d tasks", taskCount)
	}

	doc, err := dao.NewDocumentDAO().GetByID(ctx, db, "doc-1")
	if err != nil {
		t.Fatalf("get doc: %v", err)
	}
	if doc.Run == nil {
		t.Fatalf("expected doc run to remain nil, got %q", *doc.Run)
	}
	if *doc.Run != "0" {
		t.Fatalf("expected doc run status is '1', got %q", *doc.Run)
	}
	if doc.ChunkNum != 7 {
		t.Fatalf("expected chunk_num to remain 7, got %d", doc.ChunkNum)
	}
}

func TestParseRejectsInaccessibleDataset(t *testing.T) {
	svc := newParseTestService(t)
	svc.accessibleFunc = func(string, string) bool { return false }
	ctx := t.Context()
	_, code, err := svc.Parse(ctx, "user-1", "kb-1", &service.ParseFileRequest{DocumentIDs: []string{"doc-1"}})
	if err == nil {
		t.Fatal("expected parse to fail")
	}
	if code != common.CodeOperatingError {
		t.Fatalf("expected CodeOperatingError, got %v", code)
	}
	if !strings.Contains(err.Error(), "don't own the dataset") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestParseRequiresDocumentIDs(t *testing.T) {
	svc := newParseTestService(t)
	svc.accessibleFunc = func(string, string) bool { return true }
	ctx := t.Context()
	for _, req := range []*service.ParseFileRequest{nil, {DocumentIDs: nil}, {DocumentIDs: []string{}}} {
		_, code, err := svc.Parse(ctx, "user-1", "kb-1", req)
		if err == nil {
			t.Fatal("expected parse to fail")
		}
		if code != common.CodeDataError {
			t.Fatalf("expected CodeDataError, got %v", code)
		}
		if !strings.Contains(err.Error(), "document_ids") {
			t.Fatalf("unexpected error: %v", err)
		}
	}
}

func TestParseReturnsDataErrorWhenDatasetMissing(t *testing.T) {
	svc := newParseTestService(t)
	svc.accessibleFunc = func(string, string) bool { return true }
	svc.getKnowledgebaseByIDFunc = func(string) (*entity.Knowledgebase, error) { return nil, nil }
	ctx := t.Context()
	_, code, err := svc.Parse(ctx, "user-1", "kb-1", &service.ParseFileRequest{DocumentIDs: []string{"doc-1"}})
	if err == nil {
		t.Fatal("expected parse to fail")
	}
	if code != common.CodeDataError {
		t.Fatalf("expected CodeDataError, got %v", code)
	}
	if !strings.Contains(err.Error(), "dataset not found") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestParseReturnsServerErrorWhenDocumentsQueryFails(t *testing.T) {
	svc := newParseTestService(t)
	queryErr := errors.New("documents query failed")
	svc.accessibleFunc = func(string, string) bool { return true }
	svc.getKnowledgebaseByIDFunc = func(string) (*entity.Knowledgebase, error) {
		return &entity.Knowledgebase{ID: "kb-1", TenantID: "user-1"}, nil
	}
	svc.getDocumentsByIDsFunc = func([]string) ([]*entity.Document, error) {
		return nil, queryErr
	}
	ctx := t.Context()
	_, code, err := svc.Parse(ctx, "user-1", "kb-1", &service.ParseFileRequest{DocumentIDs: []string{"doc-1"}})
	if !errors.Is(err, queryErr) {
		t.Fatalf("expected query error, got %v", err)
	}
	if code != common.CodeServerError {
		t.Fatalf("expected CodeServerError, got %v", code)
	}
}

func TestParseRejectsRunningDocument(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)

	userID := "user-1"
	datasetID := "kb-1"
	insertChunkTestKB(t, datasetID, userID)
	insertChunkTestDoc(t, "doc-1", datasetID)
	running := string(entity.TaskStatusRunning)
	if err := db.Model(&entity.Document{}).Where("id = ?", "doc-1").Update("run", running).Error; err != nil {
		t.Fatalf("mark doc running: %v", err)
	}

	svc := newParseTestService(t)
	ctx := t.Context()
	_, code, err := svc.Parse(ctx, userID, datasetID, &service.ParseFileRequest{DocumentIDs: []string{"doc-1"}})
	if err == nil {
		t.Fatal("expected parse to fail")
	}
	if code != common.CodeDataError {
		t.Fatalf("expected CodeDataError, got %v", code)
	}
	if !strings.Contains(err.Error(), "currently being processed") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestListSortsChunksByDocumentPosition(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)

	userID := "user-1"
	tenantID := "tenant-1"
	datasetID := "kb-1"
	documentID := "doc-1"
	insertChunkTestUserTenant(t, userID, tenantID)
	insertChunkTestKB(t, datasetID, tenantID)
	insertChunkTestDoc(t, documentID, datasetID)

	engine := &listChunksSearchEngine{}
	svc := &ChunkService{
		docEngine:     engine,
		kbDAO:         dao.NewKnowledgebaseDAO(),
		userTenantDAO: dao.NewUserTenantDAO(),
		documentDAO:   dao.NewDocumentDAO(),
	}

	ctx := t.Context()
	page := 1
	size := 30
	if _, err := svc.List(ctx, &service.ListChunksRequest{
		DatasetID: datasetID,
		DocID:     documentID,
		Page:      &page,
		Size:      &size,
	}, userID); err != nil {
		t.Fatalf("List() error = %v", err)
	}

	if engine.searchReq == nil {
		t.Fatal("expected Search to be called")
	}
	if engine.searchReq.OrderBy == nil {
		t.Fatal("expected OrderBy to be set")
	}
	want := []types.OrderByField{
		{Field: "chunk_order_int", Type: types.SortAsc},
		{Field: "page_num_int", Type: types.SortAsc},
		{Field: "top_int", Type: types.SortAsc},
		{Field: "create_timestamp_flt", Type: types.SortDesc},
	}
	if !reflect.DeepEqual(engine.searchReq.OrderBy.Fields, want) {
		t.Fatalf("OrderBy fields = %#v, want %#v", engine.searchReq.OrderBy.Fields, want)
	}
}

func TestListBuildsMatchTextExprForKeywords(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)

	userID := "user-1"
	tenantID := "tenant-1"
	datasetID := "kb-1"
	documentID := "doc-1"
	insertChunkTestUserTenant(t, userID, tenantID)
	insertChunkTestKB(t, datasetID, tenantID)
	insertChunkTestDoc(t, documentID, datasetID)

	engine := &listChunksSearchEngine{}
	svc := &ChunkService{
		docEngine:     engine,
		kbDAO:         dao.NewKnowledgebaseDAO(),
		userTenantDAO: dao.NewUserTenantDAO(),
		documentDAO:   dao.NewDocumentDAO(),
	}
	ctx := t.Context()

	page := 2
	size := 5
	resp, err := svc.List(ctx, &service.ListChunksRequest{
		DatasetID: datasetID,
		DocID:     documentID,
		Page:      &page,
		Size:      &size,
		Keywords:  "  invoice terms  ",
	}, userID)
	if err != nil {
		t.Fatalf("List() error = %v", err)
	}
	if resp.Total != 1 {
		t.Fatalf("total = %d, want 1", resp.Total)
	}
	if engine.searchReq == nil {
		t.Fatal("expected Search to be called")
	}
	if engine.searchReq.Offset != 5 || engine.searchReq.Limit != 5 {
		t.Fatalf("pagination = offset %d limit %d, want offset 5 limit 5", engine.searchReq.Offset, engine.searchReq.Limit)
	}
	if !reflect.DeepEqual(engine.searchReq.KbIDs, []string{datasetID}) {
		t.Fatalf("KbIDs = %#v, want %#v", engine.searchReq.KbIDs, []string{datasetID})
	}
	if got := engine.searchReq.Filter["doc_id"]; got != documentID {
		t.Fatalf("doc_id filter = %#v, want %q", got, documentID)
	}
	if slices.Contains(engine.searchReq.SelectFields, "content") {
		t.Fatalf("SelectFields = %#v, should not request content with content_with_weight", engine.searchReq.SelectFields)
	}
	for _, field := range []string{"content_with_weight", "img_id", "position_int"} {
		if !slices.Contains(engine.searchReq.SelectFields, field) {
			t.Fatalf("SelectFields = %#v, missing %q", engine.searchReq.SelectFields, field)
		}
	}
	if got := resp.Chunks[0]["content_with_weight"]; got != "weighted invoice terms body" {
		t.Fatalf("formatted content_with_weight = %#v, want %q", got, "weighted invoice terms body")
	}
	if len(engine.searchReq.MatchExprs) != 1 {
		t.Fatalf("MatchExprs length = %d, want 1", len(engine.searchReq.MatchExprs))
	}
	matchText, ok := engine.searchReq.MatchExprs[0].(*types.MatchTextExpr)
	if !ok {
		t.Fatalf("MatchExprs[0] = %T, want *types.MatchTextExpr", engine.searchReq.MatchExprs[0])
	}
	if matchText.MatchingText != "invoice terms" {
		t.Fatalf("MatchingText = %q, want %q", matchText.MatchingText, "invoice terms")
	}
	if matchText.TopN != size {
		t.Fatalf("TopN = %d, want %d", matchText.TopN, size)
	}
}

func TestAddChunkSuccess(t *testing.T) {
	ctx := t.Context()
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	userID, datasetID, documentID := "user-1", "kb-1", "doc-1"
	insertChunkTestKB(t, datasetID, userID)
	insertChunkTestDoc(t, documentID, datasetID)

	engine := &addChunkTestEngine{}
	var incrementTokenNum, incrementChunkNum int64
	svc := &ChunkService{
		docEngine:   engine,
		kbDAO:       dao.NewKnowledgebaseDAO(),
		documentDAO: dao.NewDocumentDAO(),
		accessibleFunc: func(datasetIDArg, userIDArg string) bool {
			return datasetIDArg == datasetID && userIDArg == userID
		},
		getKnowledgebaseByIDFunc: func(id string) (*entity.Knowledgebase, error) {
			return &entity.Knowledgebase{ID: id, TenantID: userID, EmbdID: "embed-1"}, nil
		},
		getEmbeddingModelFunc: func(string, string) (*models.EmbeddingModel, error) {
			driver := &stubEmbeddingDriver{
				embeddings: []models.EmbeddingData{
					{Embedding: []float64{1, 2}},
					{Embedding: []float64{3, 4}},
				},
			}
			modelName := "embed-1"
			return models.NewEmbeddingModel(driver, &modelName, &models.APIConfig{}, 0), nil
		},
		incrementChunkStatsFunc: func(docID, kbID string, tokenNum, chunkNum int64, duration float64) error {
			if docID != documentID || kbID != datasetID || duration != 0 {
				t.Fatalf("unexpected increment args doc=%s kb=%s duration=%v", docID, kbID, duration)
			}
			incrementTokenNum = tokenNum
			incrementChunkNum = chunkNum
			return nil
		},
		tokenizeFunc:            func(text string) (string, error) { return text, nil },
		fineGrainedTokenizeFunc: func(text string) (string, error) { return text + "_fg", nil },
		numTokensFunc:           func(text string) int { return len(text) },
	}

	resp, err := svc.AddChunk(ctx, &service.AddChunkRequest{
		DatasetID:         datasetID,
		DocumentID:        documentID,
		Content:           "chunk body",
		ImportantKeywords: []string{"k1"},
		Questions:         []string{" q1 ", ""},
		TagKwd:            []string{"tag1"},
		TagFeas:           map[string]interface{}{"tag1": float64(0.5)},
	}, userID)
	if err != nil {
		t.Fatalf("AddChunk() error = %v", err)
	}
	if resp == nil || resp.Chunk == nil {
		t.Fatalf("expected chunk response, got %#v", resp)
	}
	if resp.Chunk["dataset_id"] != datasetID || resp.Chunk["document_id"] != documentID {
		t.Fatalf("unexpected response chunk: %#v", resp.Chunk)
	}
	if resp.Chunk["content"] != "chunk body" {
		t.Fatalf("content = %v, want chunk body", resp.Chunk["content"])
	}
	if resp.Chunk["document"] != "doc-1.txt" {
		t.Fatalf("document = %v, want doc-1.txt", resp.Chunk["document"])
	}
	if incrementChunkNum != 1 {
		t.Fatalf("increment chunk num = %d, want 1", incrementChunkNum)
	}
	if incrementTokenNum <= 0 {
		t.Fatalf("increment token num = %d, want > 0", incrementTokenNum)
	}
	if len(engine.insertedChunks) != 1 {
		t.Fatalf("inserted chunks = %d, want 1", len(engine.insertedChunks))
	}
	inserted := engine.insertedChunks[0]
	if inserted["doc_id"] != documentID || inserted["kb_id"] != datasetID {
		t.Fatalf("unexpected inserted chunk: %#v", inserted)
	}
	if inserted["img_id"] != nil {
		t.Fatalf("did not expect image id in inserted chunk: %#v", inserted)
	}
	vec, ok := inserted["q_2_vec"].([]float64)
	if !ok {
		t.Fatalf("expected q_2_vec []float64, got %T", inserted["q_2_vec"])
	}
	if len(vec) != 2 || vec[0] < 2.7999 || vec[0] > 2.8001 || vec[1] < 3.7999 || vec[1] > 3.8001 {
		t.Fatalf("vector = %v, want approximately [2.8 3.8]", vec)
	}
}

func TestAddChunkValidationErrors(t *testing.T) {
	ctx := t.Context()
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	insertChunkTestKB(t, "kb-1", "user-1")
	insertChunkTestDoc(t, "doc-1", "kb-1")

	svc := &ChunkService{
		docEngine:   &addChunkTestEngine{},
		kbDAO:       dao.NewKnowledgebaseDAO(),
		documentDAO: dao.NewDocumentDAO(),
	}

	tests := []struct {
		name    string
		req     *service.AddChunkRequest
		setup   func(*ChunkService)
		wantMsg string
	}{
		{
			name:    "nil request",
			req:     nil,
			wantMsg: "invalid request payload",
		},
		{
			name: "empty content",
			req:  &service.AddChunkRequest{DatasetID: "kb-1", DocumentID: "doc-1", Content: " "},
			setup: func(svc *ChunkService) {
				svc.accessibleFunc = func(string, string) bool { return true }
				svc.getKnowledgebaseByIDFunc = func(string) (*entity.Knowledgebase, error) {
					return &entity.Knowledgebase{ID: "kb-1", TenantID: "user-1", EmbdID: "embed-1"}, nil
				}
			},
			wantMsg: "`content` is required",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.setup != nil {
				tt.setup(svc)
			}
			_, err := svc.AddChunk(ctx, tt.req, "user-1")
			if err == nil || !strings.Contains(err.Error(), tt.wantMsg) {
				t.Fatalf("error = %v, want substring %q", err, tt.wantMsg)
			}
		})
	}
}

func TestAddChunkImageAndTagFeatureValidation(t *testing.T) {
	ctx := t.Context()
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	userID, datasetID, documentID := "user-1", "kb-1", "doc-1"
	insertChunkTestKB(t, datasetID, userID)
	insertChunkTestDoc(t, documentID, datasetID)

	storeCalls := 0
	svc := &ChunkService{
		docEngine:      &addChunkTestEngine{},
		kbDAO:          dao.NewKnowledgebaseDAO(),
		documentDAO:    dao.NewDocumentDAO(),
		accessibleFunc: func(string, string) bool { return true },
		getKnowledgebaseByIDFunc: func(id string) (*entity.Knowledgebase, error) {
			return &entity.Knowledgebase{ID: id, TenantID: userID, EmbdID: "embed-1"}, nil
		},
		tokenizeFunc:            func(text string) (string, error) { return text, nil },
		fineGrainedTokenizeFunc: func(text string) (string, error) { return text + "_fg", nil },
		numTokensFunc:           func(text string) int { return len(text) },
		getEmbeddingModelFunc: func(string, string) (*models.EmbeddingModel, error) {
			driver := &stubEmbeddingDriver{
				embeddings: []models.EmbeddingData{
					{Embedding: []float64{1, 1}},
					{Embedding: []float64{1, 1}},
				},
			}
			modelName := "embed-1"
			return models.NewEmbeddingModel(driver, &modelName, &models.APIConfig{}, 0), nil
		},
		incrementChunkStatsFunc: func(string, string, int64, int64, float64) error { return nil },
		storeChunkImageFunc: func(bucket, chunkID string, imageBinary []byte) error {
			storeCalls++
			if bucket != datasetID || chunkID == "" || len(imageBinary) == 0 {
				t.Fatalf("unexpected store args bucket=%s chunkID=%s len=%d", bucket, chunkID, len(imageBinary))
			}
			return nil
		},
	}

	_, err := svc.AddChunk(ctx, &service.AddChunkRequest{
		DatasetID:   datasetID,
		DocumentID:  documentID,
		Content:     "chunk body",
		ImageBase64: strPtr("not-base64"),
	}, userID)
	if err == nil || !strings.Contains(err.Error(), "invalid `image_base64`") {
		t.Fatalf("expected invalid image error, got %v", err)
	}

	validJPEG := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO2pRZ0AAAAASUVORK5CYII="

	resp, err := svc.AddChunk(ctx, &service.AddChunkRequest{
		DatasetID:  datasetID,
		DocumentID: documentID,
		Content:    "chunk body",
		TagFeas:    map[string]interface{}{"tag1": "bad"},
	}, userID)
	if err == nil || !strings.Contains(err.Error(), "`tag_feas` values must be finite numbers") || resp != nil {
		t.Fatalf("expected tag_feas validation error, got resp=%#v err=%v", resp, err)
	}

	resp, err = svc.AddChunk(ctx, &service.AddChunkRequest{
		DatasetID:   datasetID,
		DocumentID:  documentID,
		Content:     "chunk body",
		TagFeas:     map[string]interface{}{"tag1": float64(1)},
		ImageBase64: strPtr(validJPEG),
	}, userID)
	if err != nil {
		t.Fatalf("AddChunk() with image error = %v", err)
	}
	if storeCalls != 1 {
		t.Fatalf("store image calls = %d, want 1", storeCalls)
	}
	if _, ok := resp.Chunk["image_id"]; !ok {
		t.Fatalf("expected image_id in response, got %#v", resp.Chunk)
	}
}

func TestAddChunkIncrementsStatsAfterInsert(t *testing.T) {
	ctx := t.Context()
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	userID, datasetID, documentID := "user-1", "kb-1", "doc-1"
	insertChunkTestKB(t, datasetID, userID)
	insertChunkTestDoc(t, documentID, datasetID)

	var incrementCalls int
	engine := &addChunkTestEngine{}
	svc := &ChunkService{
		docEngine:      engine,
		kbDAO:          dao.NewKnowledgebaseDAO(),
		documentDAO:    dao.NewDocumentDAO(),
		accessibleFunc: func(string, string) bool { return true },
		getKnowledgebaseByIDFunc: func(id string) (*entity.Knowledgebase, error) {
			return &entity.Knowledgebase{ID: id, TenantID: userID, EmbdID: "embed-1"}, nil
		},
		getEmbeddingModelFunc: func(string, string) (*models.EmbeddingModel, error) {
			driver := &stubEmbeddingDriver{
				embeddings: []models.EmbeddingData{
					{Embedding: []float64{1, 2}},
					{Embedding: []float64{3, 4}},
				},
			}
			modelName := "embed-1"
			return models.NewEmbeddingModel(driver, &modelName, &models.APIConfig{}, 0), nil
		},
		incrementChunkStatsFunc: func(string, string, int64, int64, float64) error {
			incrementCalls++
			return nil
		},
		tokenizeFunc:            func(text string) (string, error) { return text, nil },
		fineGrainedTokenizeFunc: func(text string) (string, error) { return text + "_fg", nil },
		numTokensFunc:           func(text string) int { return len(text) },
	}

	_, err := svc.AddChunk(ctx, &service.AddChunkRequest{
		DatasetID:  datasetID,
		DocumentID: documentID,
		Content:    "chunk body",
	}, userID)
	if err != nil {
		t.Fatalf("AddChunk() error = %v", err)
	}
	if incrementCalls != 1 {
		t.Fatalf("increment calls = %d, want 1", incrementCalls)
	}
	if len(engine.insertedChunks) != 1 {
		t.Fatalf("inserted chunks = %d, want 1", len(engine.insertedChunks))
	}
	importantKwd, ok := engine.insertedChunks[0]["important_kwd"].([]string)
	if !ok || len(importantKwd) != 0 {
		t.Fatalf("important_kwd = %#v, want empty []string", engine.insertedChunks[0]["important_kwd"])
	}
}

func TestStoreChunkImageMergesExistingImage(t *testing.T) {
	oldImage := mustEncodePNG(t, image.Rect(0, 0, 2, 2))
	newImage := mustEncodePNG(t, image.Rect(0, 0, 1, 1))
	mockStorage := &chunkImageStorage{
		exists:    true,
		oldBinary: oldImage,
	}

	factory := storage.GetStorageFactory()
	originalStorage := factory.GetStorage()
	factory.SetStorage(mockStorage)
	t.Cleanup(func() {
		factory.SetStorage(originalStorage)
	})

	svc := &ChunkService{}
	if err := svc.storeChunkImage("kb-1", "chunk-1", newImage); err != nil {
		t.Fatalf("storeChunkImage() error = %v", err)
	}
	if mockStorage.putCalls != 1 {
		t.Fatalf("put calls = %d, want 1", mockStorage.putCalls)
	}
}

func TestRemoveChunksDecrementsStatsAfterDelete(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	insertChunkTestUserTenant(t, "user-1", "tenant-1")
	insertChunkTestKB(t, "kb-1", "tenant-1")
	insertChunkTestDoc(t, "doc-1", "kb-1")
	setChunkTestStats(t, "doc-1", "kb-1", 70, 7, 100, 10)

	engine := &parseTestDocEngine{deleteChunksCount: 3}
	svc := &ChunkService{
		docEngine:     engine,
		kbDAO:         dao.NewKnowledgebaseDAO(),
		userTenantDAO: dao.NewUserTenantDAO(),
	}
	ctx := t.Context()
	deletedCount, err := svc.RemoveChunks(ctx, &service.RemoveChunksRequest{
		DocID:    "doc-1",
		ChunkIDs: []string{"chunk-1", "chunk-2", "chunk-3"},
	}, "user-1")
	if err != nil {
		t.Fatalf("RemoveChunks() error = %v", err)
	}
	if deletedCount != 3 {
		t.Fatalf("deleted count = %d, want 3", deletedCount)
	}
	if engine.deleteChunksCalls != 1 {
		t.Fatalf("DeleteChunks calls = %d, want 1", engine.deleteChunksCalls)
	}
	if engine.deleteIndexName != "ragflow_tenant-1" || engine.deleteDatasetID != "kb-1" {
		t.Fatalf("unexpected delete target index=%q dataset=%q", engine.deleteIndexName, engine.deleteDatasetID)
	}
	if engine.deleteChunksCondition["doc_id"] != "doc-1" {
		t.Fatalf("delete doc_id condition = %#v, want doc-1", engine.deleteChunksCondition["doc_id"])
	}
	if !reflect.DeepEqual(engine.deleteChunksCondition["id"], []interface{}{"chunk-1", "chunk-2", "chunk-3"}) {
		t.Fatalf("delete id condition = %#v", engine.deleteChunksCondition["id"])
	}

	doc, err := dao.NewDocumentDAO().GetByID(ctx, db, "doc-1")
	if err != nil {
		t.Fatalf("get doc: %v", err)
	}
	if doc.TokenNum != 70 {
		t.Fatalf("document token_num = %d, want 70", doc.TokenNum)
	}
	if doc.ChunkNum != 4 {
		t.Fatalf("document chunk_num = %d, want 4", doc.ChunkNum)
	}

	kb, err := dao.NewKnowledgebaseDAO().GetByID(ctx, db, "kb-1")
	if err != nil {
		t.Fatalf("get kb: %v", err)
	}
	if kb.TokenNum != 100 {
		t.Fatalf("knowledgebase token_num = %d, want 100", kb.TokenNum)
	}
	if kb.ChunkNum != 7 {
		t.Fatalf("knowledgebase chunk_num = %d, want 7", kb.ChunkNum)
	}
}

func TestRemoveChunksSkipsStatsWhenNothingDeleted(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	insertChunkTestUserTenant(t, "user-1", "tenant-1")
	insertChunkTestKB(t, "kb-1", "tenant-1")
	insertChunkTestDoc(t, "doc-1", "kb-1")
	setChunkTestStats(t, "doc-1", "kb-1", 70, 7, 100, 10)

	var decrementCalls int
	svc := &ChunkService{
		docEngine:     &parseTestDocEngine{deleteChunksCount: 0},
		kbDAO:         dao.NewKnowledgebaseDAO(),
		userTenantDAO: dao.NewUserTenantDAO(),
		decrementChunkStatsFunc: func(string, string, int64, int64, float64) error {
			decrementCalls++
			return nil
		},
	}
	ctx := t.Context()
	deletedCount, err := svc.RemoveChunks(ctx, &service.RemoveChunksRequest{
		DocID:     "doc-1",
		DeleteAll: true,
	}, "user-1")
	if err != nil {
		t.Fatalf("RemoveChunks() error = %v", err)
	}
	if deletedCount != 0 {
		t.Fatalf("deleted count = %d, want 0", deletedCount)
	}
	if decrementCalls != 0 {
		t.Fatalf("decrement calls = %d, want 0", decrementCalls)
	}
}

func TestRemoveChunksReturnsStatsError(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	insertChunkTestUserTenant(t, "user-1", "tenant-1")
	insertChunkTestKB(t, "kb-1", "tenant-1")
	insertChunkTestDoc(t, "doc-1", "kb-1")

	svc := &ChunkService{
		docEngine:     &parseTestDocEngine{deleteChunksCount: 2},
		kbDAO:         dao.NewKnowledgebaseDAO(),
		userTenantDAO: dao.NewUserTenantDAO(),
		decrementChunkStatsFunc: func(docID, kbID string, tokenNum, chunkNum int64, duration float64) error {
			if docID != "doc-1" || kbID != "kb-1" || tokenNum != 0 || chunkNum != 2 || duration != 0 {
				t.Fatalf("unexpected decrement args doc=%s kb=%s token=%d chunk=%d duration=%v", docID, kbID, tokenNum, chunkNum, duration)
			}
			return errors.New("stats update failed")
		},
	}
	ctx := t.Context()
	deletedCount, err := svc.RemoveChunks(ctx, &service.RemoveChunksRequest{
		DocID:    "doc-1",
		ChunkIDs: []string{"chunk-1", "chunk-2"},
	}, "user-1")
	if err == nil || !strings.Contains(err.Error(), "failed to update chunk stats") {
		t.Fatalf("expected stats update error, got count=%d err=%v", deletedCount, err)
	}
	if deletedCount != 2 {
		t.Fatalf("deleted count on error = %d, want 2", deletedCount)
	}
}

func TestDecrementChunkStatsClampsCounters(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	insertChunkTestKB(t, "kb-1", "tenant-1")
	insertChunkTestDoc(t, "doc-1", "kb-1")
	setChunkTestStats(t, "doc-1", "kb-1", 2, 1, 3, 2)
	if err := dao.DB.Model(&entity.Document{}).
		Where("id = ?", "doc-1").
		Update("process_duration", 0.5).Error; err != nil {
		t.Fatalf("set process duration: %v", err)
	}

	svc := &ChunkService{}
	if err := svc.decrementChunkStats("doc-1", "kb-1", 5, 7, -1); err != nil {
		t.Fatalf("decrementChunkStats() error = %v", err)
	}
	ctx := t.Context()
	doc, err := dao.NewDocumentDAO().GetByID(ctx, db, "doc-1")
	if err != nil {
		t.Fatalf("get doc: %v", err)
	}
	if doc.TokenNum != 0 || doc.ChunkNum != 0 || doc.ProcessDuration != 0 {
		t.Fatalf("document stats token=%d chunk=%d duration=%v, want zeros", doc.TokenNum, doc.ChunkNum, doc.ProcessDuration)
	}

	kb, err := dao.NewKnowledgebaseDAO().GetByID(ctx, db, "kb-1")
	if err != nil {
		t.Fatalf("get kb: %v", err)
	}
	if kb.TokenNum != 0 || kb.ChunkNum != 0 {
		t.Fatalf("knowledgebase stats token=%d chunk=%d, want zeros", kb.TokenNum, kb.ChunkNum)
	}
}

func TestDecrementChunkStatsRollsBackWhenKnowledgebaseMissing(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	insertChunkTestDoc(t, "doc-1", "missing-kb")

	svc := &ChunkService{}
	err := svc.decrementChunkStats("doc-1", "missing-kb", 0, 3, 0)
	if err == nil || !strings.Contains(err.Error(), "knowledgebase not found") {
		t.Fatalf("expected missing knowledgebase error, got %v", err)
	}
	ctx := t.Context()
	doc, err := dao.NewDocumentDAO().GetByID(ctx, db, "doc-1")
	if err != nil {
		t.Fatalf("get doc: %v", err)
	}
	if doc.ChunkNum != 7 {
		t.Fatalf("document chunk_num after rollback = %d, want 7", doc.ChunkNum)
	}
}

func setupChunkTestDB(t *testing.T) *gorm.DB {
	t.Helper()

	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
		TranslateError: true,
	})
	if err != nil {
		t.Fatalf("failed to open sqlite: %v", err)
	}
	if err := db.AutoMigrate(
		&entity.UserTenant{},
		&entity.Knowledgebase{},
		&entity.Document{},
		&entity.Task{},
		&entity.IngestionTask{},
	); err != nil {
		t.Fatalf("failed to migrate: %v", err)
	}
	return db
}

func pushChunkTestDB(t *testing.T, testDB *gorm.DB) {
	t.Helper()

	orig := dao.DB
	dao.DB = testDB
	t.Cleanup(func() {
		dao.DB = orig
	})
}

func newParseTestService(t *testing.T) *ChunkService {
	t.Helper()

	return &ChunkService{
		docEngine:   nil,
		kbDAO:       dao.NewKnowledgebaseDAO(),
		documentDAO: dao.NewDocumentDAO(),
	}
}

func insertChunkTestUserTenant(t *testing.T, userID, tenantID string) {
	t.Helper()

	status := "1"
	userTenant := &entity.UserTenant{
		ID:        userID + "-" + tenantID,
		UserID:    userID,
		TenantID:  tenantID,
		Role:      "owner",
		InvitedBy: userID,
		Status:    &status,
	}
	if err := dao.DB.Create(userTenant).Error; err != nil {
		t.Fatalf("insert user tenant: %v", err)
	}
}

func insertChunkTestKB(t *testing.T, id, tenantID string) {
	t.Helper()

	status := string(entity.StatusValid)
	kb := &entity.Knowledgebase{
		ID:           id,
		TenantID:     tenantID,
		Name:         id,
		EmbdID:       "embedding-model",
		Permission:   string(entity.TenantPermissionMe),
		CreatedBy:    tenantID,
		ParserConfig: entity.JSONMap{},
		Status:       &status,
	}
	if err := dao.DB.Create(kb).Error; err != nil {
		t.Fatalf("insert kb: %v", err)
	}
}

func insertChunkTestDoc(t *testing.T, id, kbID string) {
	t.Helper()

	location := id + ".txt"
	name := id + ".txt"
	status := string(entity.StatusValid)
	doc := &entity.Document{
		ID:           id,
		KbID:         kbID,
		ParserID:     string(entity.ParserTypeNaive),
		ParserConfig: entity.JSONMap{},
		SourceType:   string(entity.FileSourceLocal),
		Type:         "txt",
		CreatedBy:    "user-1",
		Name:         &name,
		Location:     &location,
		ChunkNum:     7,
		Suffix:       ".txt",
		Status:       &status,
	}
	if err := dao.DB.Create(doc).Error; err != nil {
		t.Fatalf("insert doc: %v", err)
	}
}

func insertChunkTestIngestionTask(t *testing.T, id, userID, docID, datasetID, status string) {
	t.Helper()
	task := &entity.IngestionTask{
		ID:         id,
		UserID:     userID,
		DocumentID: docID,
		DatasetID:  datasetID,
		Schema:     entity.JSONMap{},
		Status:     status,
	}
	if err := dao.DB.Create(task).Error; err != nil {
		t.Fatalf("insert ingestion task: %v", err)
	}
}

func setChunkTestStats(t *testing.T, docID, kbID string, docTokenNum, docChunkNum, kbTokenNum, kbChunkNum int64) {
	t.Helper()

	if err := dao.DB.Model(&entity.Document{}).
		Where("id = ?", docID).
		Updates(map[string]interface{}{"token_num": docTokenNum, "chunk_num": docChunkNum}).Error; err != nil {
		t.Fatalf("set doc stats: %v", err)
	}
	if err := dao.DB.Model(&entity.Knowledgebase{}).
		Where("id = ?", kbID).
		Updates(map[string]interface{}{"token_num": kbTokenNum, "chunk_num": kbChunkNum}).Error; err != nil {
		t.Fatalf("set kb stats: %v", err)
	}
}

func insertChunkTestTask(t *testing.T, id, docID string) {
	t.Helper()

	digest := "digest"
	task := &entity.Task{
		ID:     id,
		DocID:  docID,
		Digest: &digest,
	}
	if err := dao.DB.Create(task).Error; err != nil {
		t.Fatalf("insert task: %v", err)
	}
}

type parseTestDocEngine struct {
	deleteChunksErr       error
	deleteChunksCount     int64
	deleteChunksCalls     int
	deleteChunksCondition map[string]interface{}
	deleteIndexName       string
	deleteDatasetID       string
	chunkStoreExists      bool // if true, ChunkStoreExists returns true
}

func (e *parseTestDocEngine) CreateChunkStore(context.Context, string, string, int, string) error {
	return nil
}

func (e *parseTestDocEngine) InsertChunks(context.Context, []map[string]interface{}, string, string) ([]string, error) {
	return nil, nil
}

func (e *parseTestDocEngine) UpdateChunks(context.Context, map[string]interface{}, map[string]interface{}, string, string) error {
	return nil
}

func (e *parseTestDocEngine) DeleteChunks(_ context.Context, condition map[string]interface{}, indexName string, datasetID string) (int64, error) {
	e.deleteChunksCalls++
	e.deleteChunksCondition = copyMap(condition)
	e.deleteIndexName = indexName
	e.deleteDatasetID = datasetID
	return e.deleteChunksCount, e.deleteChunksErr
}

func (e *parseTestDocEngine) Search(context.Context, *types.SearchRequest) (*types.SearchResult, error) {
	return nil, nil
}

func (e *parseTestDocEngine) GetChunk(context.Context, string, string, []string) (interface{}, error) {
	return nil, nil
}

func (e *parseTestDocEngine) DropChunkStore(context.Context, string, string) error {
	return nil
}

func (e *parseTestDocEngine) ChunkStoreExists(context.Context, string, string) (bool, error) {
	return e.chunkStoreExists, nil
}

func (e *parseTestDocEngine) CreateMetadataStore(context.Context, string) error {
	return nil
}

func (e *parseTestDocEngine) InsertMetadata(context.Context, []map[string]interface{}, string) ([]string, error) {
	return nil, nil
}

func (e *parseTestDocEngine) UpdateMetadata(context.Context, string, string, map[string]interface{}, string) error {
	return nil
}

func (e *parseTestDocEngine) DeleteMetadata(context.Context, map[string]interface{}, string) (int64, error) {
	return 0, nil
}

func (e *parseTestDocEngine) DeleteMetadataKeys(context.Context, string, string, []string, string) error {
	return nil
}

func (e *parseTestDocEngine) DropMetadataStore(context.Context, string) error {
	return nil
}

func (e *parseTestDocEngine) MetadataStoreExists(context.Context, string) (bool, error) {
	return false, nil
}

func (e *parseTestDocEngine) SearchMetadata(context.Context, *types.SearchMetadataRequest) (*types.SearchMetadataResult, error) {
	return nil, nil
}

func (e *parseTestDocEngine) IndexDocument(context.Context, string, string, interface{}) error {
	return nil
}

func (e *parseTestDocEngine) DeleteDocument(context.Context, string, string) error {
	return nil
}

func (e *parseTestDocEngine) BulkIndex(context.Context, string, []interface{}) (interface{}, error) {
	return nil, nil
}

func (e *parseTestDocEngine) GetFields([]map[string]interface{}, []string) map[string]map[string]interface{} {
	return nil
}

func (e *parseTestDocEngine) GetAggregation([]map[string]interface{}, string) []map[string]interface{} {
	return nil
}

func (e *parseTestDocEngine) GetHighlight([]map[string]interface{}, []string, string) map[string]string {
	return nil
}

func (e *parseTestDocEngine) RunSQL(context.Context, string, string, []string, string) ([]map[string]interface{}, error) {
	return nil, nil
}

func (e *parseTestDocEngine) GetChunkIDs([]map[string]interface{}) []string {
	return nil
}

type addChunkTestEngine struct {
	parseTestDocEngine
	insertedChunks []map[string]interface{}
	insertIndex    string
	insertDataset  string
	insertErr      error
}

func (e *addChunkTestEngine) InsertChunks(_ context.Context, chunks []map[string]interface{}, baseName string, datasetID string) ([]string, error) {
	e.insertedChunks = chunks
	e.insertIndex = baseName
	e.insertDataset = datasetID
	return nil, e.insertErr
}

type listChunksSearchEngine struct {
	parseTestDocEngine
	searchReq *types.SearchRequest
}

func (e *listChunksSearchEngine) Search(_ context.Context, req *types.SearchRequest) (*types.SearchResult, error) {
	e.searchReq = req
	return &types.SearchResult{
		Chunks: []map[string]interface{}{
			{
				"id":                  "chunk-1",
				"content":             "plain invoice terms body",
				"content_with_weight": "weighted invoice terms body",
				"doc_id":              "doc-1",
			},
		},
		Total: 1,
	}, nil
}

type chunkImageStorage struct {
	exists    bool
	oldBinary []byte
	putCalls  int
}

func (s *chunkImageStorage) Type() string { return "chunk_image_storage" }
func (s *chunkImageStorage) Health() bool { return true }
func (s *chunkImageStorage) Put(bucket, fnm string, binary []byte, tenantID ...string) error {
	s.putCalls++
	return nil
}
func (s *chunkImageStorage) Get(bucket, fnm string, tenantID ...string) ([]byte, error) {
	return s.oldBinary, nil
}
func (s *chunkImageStorage) Remove(bucket, fnm string, tenantID ...string) error  { return nil }
func (s *chunkImageStorage) ObjExist(bucket, fnm string, tenantID ...string) bool { return s.exists }

// ListObjects lists all objects in a bucket
func (s *chunkImageStorage) ListObjects(bucket string, tenantID ...string) ([]string, error) {
	return []string{}, nil
}
func (s *chunkImageStorage) GetPresignedURL(bucket, fnm string, expires time.Duration, tenantID ...string) (string, error) {
	return "", nil
}
func (s *chunkImageStorage) BucketExists(bucket string) bool                           { return true }
func (s *chunkImageStorage) RemoveBucket(bucket string) error                          { return nil }
func (s *chunkImageStorage) Copy(srcBucket, srcPath, destBucket, destPath string) bool { return false }
func (s *chunkImageStorage) Move(srcBucket, srcPath, destBucket, destPath string) bool { return false }
func (s *chunkImageStorage) Close() error                                              { return nil }

func mustEncodePNG(t *testing.T, rect image.Rectangle) []byte {
	t.Helper()

	img := image.NewRGBA(rect)
	for y := rect.Min.Y; y < rect.Max.Y; y++ {
		for x := rect.Min.X; x < rect.Max.X; x++ {
			img.Set(x, y, color.White)
		}
	}

	var buf bytes.Buffer
	if err := png.Encode(&buf, img); err != nil {
		t.Fatalf("encode png: %v", err)
	}
	return buf.Bytes()
}

type stubEmbeddingDriver struct {
	embeddings []models.EmbeddingData
	embedErr   error
}

func (d *stubEmbeddingDriver) NewInstance(map[string]string) models.ModelDriver { return d }
func (d *stubEmbeddingDriver) Name() string                                     { return "stub" }
func (d *stubEmbeddingDriver) ChatWithMessages(context.Context, string, []models.Message, *models.APIConfig, *models.ChatConfig, *common.ModelUsage) (*models.ChatResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) ChatStreamlyWithSender(context.Context, string, []models.Message, *models.APIConfig, *models.ChatConfig, *common.ModelUsage, func(*string, *string) error) error {
	return nil
}
func (d *stubEmbeddingDriver) Embed(context.Context, *string, []string, *models.APIConfig, *models.EmbeddingConfig, *common.ModelUsage) ([]models.EmbeddingData, error) {
	return d.embeddings, d.embedErr
}
func (d *stubEmbeddingDriver) Rerank(context.Context, *string, string, []string, *models.APIConfig, *models.RerankConfig, *common.ModelUsage) (*models.RerankResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) TranscribeAudio(context.Context, *string, *string, *models.APIConfig, *models.ASRConfig, *common.ModelUsage) (*models.ASRResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) TranscribeAudioWithSender(context.Context, *string, *string, *models.APIConfig, *models.ASRConfig, *common.ModelUsage, func(*string, *string) error) error {
	return nil
}
func (d *stubEmbeddingDriver) AudioSpeech(context.Context, *string, *string, *models.APIConfig, *models.TTSConfig, *common.ModelUsage) (*models.TTSResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) AudioSpeechWithSender(context.Context, *string, *string, *models.APIConfig, *models.TTSConfig, *common.ModelUsage, func(*string, *string) error) error {
	return nil
}
func (d *stubEmbeddingDriver) OCRFile(context.Context, *string, []byte, *string, *models.APIConfig, *models.OCRConfig, *common.ModelUsage) (*models.OCRFileResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) ParseFile(context.Context, *string, []byte, *string, *models.APIConfig, *models.ParseFileConfig, *common.ModelUsage) (*models.ParseFileResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) ListModels(context.Context, *models.APIConfig) ([]models.ListModelResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) Balance(context.Context, *models.APIConfig) (map[string]interface{}, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) CheckConnection(context.Context, *models.APIConfig) error { return nil }
func (d *stubEmbeddingDriver) ListTasks(context.Context, *models.APIConfig) ([]models.ListTaskStatus, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) ShowTask(context.Context, string, *models.APIConfig) (*models.TaskResponse, error) {
	return nil, nil
}

func strPtr(v string) *string {
	return &v
}

func (e *parseTestDocEngine) KNNScores(context.Context, []map[string]interface{}, []float64, int) (map[string]interface{}, error) {
	return nil, nil
}

func (e *parseTestDocEngine) GetScores(map[string]interface{}) map[string]float64 {
	return nil
}

func (e *parseTestDocEngine) Ping(context.Context) error {
	return nil
}

func (e *parseTestDocEngine) Close() error {
	return nil
}

func (e *parseTestDocEngine) GetType() string {
	return "test"
}
func (e *parseTestDocEngine) SupportsPageRank() bool { return false }

func (e *parseTestDocEngine) FilterDocIdsByMetaPushdown(context.Context, *gorm.DB, []string, []map[string]interface{}, string) []string {
	return nil
}

func TestSwitchChunksUpdatesDocEngineWithAvailableInt(t *testing.T) {
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	if err != nil {
		t.Fatalf("failed to open sqlite: %v", err)
	}
	if err := db.AutoMigrate(&entity.UserTenant{}, &entity.Knowledgebase{}, &entity.Document{}); err != nil {
		t.Fatalf("failed to migrate sqlite: %v", err)
	}
	previousDB := dao.DB
	dao.DB = db
	t.Cleanup(func() { dao.DB = previousDB })

	valid := string(entity.StatusValid)
	if err := db.Create(&entity.UserTenant{
		ID:       "ut-1",
		UserID:   "user-1",
		TenantID: "tenant-1",
		Role:     "owner",
		Status:   &valid,
	}).Error; err != nil {
		t.Fatalf("failed to create user_tenant: %v", err)
	}
	if err := db.Create(&entity.Knowledgebase{
		ID:           "kb-1",
		TenantID:     "tenant-1",
		Name:         "dataset",
		EmbdID:       "embed",
		Permission:   string(entity.TenantPermissionMe),
		CreatedBy:    "user-1",
		ParserID:     string(entity.ParserTypeNaive),
		ParserConfig: entity.JSONMap{},
		Status:       &valid,
	}).Error; err != nil {
		t.Fatalf("failed to create knowledgebase: %v", err)
	}
	if err := db.Create(&entity.Document{
		ID:           "doc-1",
		KbID:         "kb-1",
		ParserID:     string(entity.ParserTypeNaive),
		ParserConfig: entity.JSONMap{},
		SourceType:   "local",
		Type:         "doc",
		CreatedBy:    "user-1",
		Suffix:       "txt",
	}).Error; err != nil {
		t.Fatalf("failed to create document: %v", err)
	}

	engine := &switchChunksEngineMock{}
	svc := &ChunkService{
		docEngine:     engine,
		kbDAO:         dao.NewKnowledgebaseDAO(),
		userTenantDAO: dao.NewUserTenantDAO(),
	}

	ctx := t.Context()
	if err = svc.SwitchChunks(ctx, "user-1", "kb-1", "doc-1", 0, []string{"chunk-1", "chunk-2"}); err != nil {
		t.Fatalf("SwitchChunks() error = %v", err)
	}

	if len(engine.updateCalls) != 2 {
		t.Fatalf("UpdateChunks calls = %d, want 2", len(engine.updateCalls))
	}
	for i, call := range engine.updateCalls {
		if call.indexName != "ragflow_tenant-1" {
			t.Fatalf("call %d indexName = %q", i, call.indexName)
		}
		if call.datasetID != "kb-1" {
			t.Fatalf("call %d datasetID = %q", i, call.datasetID)
		}
		wantID := []string{"chunk-1", "chunk-2"}[i]
		if !reflect.DeepEqual(call.condition, map[string]interface{}{
			"id":     wantID,
			"doc_id": "doc-1",
		}) {
			t.Fatalf("call %d condition = %#v", i, call.condition)
		}
		if !reflect.DeepEqual(call.newValue, map[string]interface{}{"id": wantID, "available_int": 0}) {
			t.Fatalf("call %d newValue = %#v", i, call.newValue)
		}
	}
}

type updateChunksCall struct {
	condition map[string]interface{}
	newValue  map[string]interface{}
	indexName string
	datasetID string
}

type switchChunksEngineMock struct {
	updateCalls []updateChunksCall
}

func (m *switchChunksEngineMock) CreateChunkStore(context.Context, string, string, int, string) error {
	return nil
}
func (m *switchChunksEngineMock) InsertChunks(context.Context, []map[string]interface{}, string, string) ([]string, error) {
	return nil, nil
}
func (m *switchChunksEngineMock) UpdateChunks(_ context.Context, condition map[string]interface{}, newValue map[string]interface{}, indexName string, datasetID string) error {
	m.updateCalls = append(m.updateCalls, updateChunksCall{
		condition: copyMap(condition),
		newValue:  copyMap(newValue),
		indexName: indexName,
		datasetID: datasetID,
	})
	return nil
}
func (m *switchChunksEngineMock) DeleteChunks(context.Context, map[string]interface{}, string, string) (int64, error) {
	return 0, nil
}
func (m *switchChunksEngineMock) Search(context.Context, *types.SearchRequest) (*types.SearchResult, error) {
	return nil, nil
}
func (m *switchChunksEngineMock) GetChunk(context.Context, string, string, []string) (interface{}, error) {
	return nil, nil
}
func (m *switchChunksEngineMock) DropChunkStore(context.Context, string, string) error { return nil }
func (m *switchChunksEngineMock) ChunkStoreExists(context.Context, string, string) (bool, error) {
	return false, nil
}
func (m *switchChunksEngineMock) CreateMetadataStore(context.Context, string) error { return nil }
func (m *switchChunksEngineMock) InsertMetadata(context.Context, []map[string]interface{}, string) ([]string, error) {
	return nil, nil
}
func (m *switchChunksEngineMock) UpdateMetadata(context.Context, string, string, map[string]interface{}, string) error {
	return nil
}
func (m *switchChunksEngineMock) DeleteMetadata(context.Context, map[string]interface{}, string) (int64, error) {
	return 0, nil
}
func (m *switchChunksEngineMock) DeleteMetadataKeys(context.Context, string, string, []string, string) error {
	return nil
}
func (m *switchChunksEngineMock) DropMetadataStore(context.Context, string) error { return nil }
func (m *switchChunksEngineMock) MetadataStoreExists(context.Context, string) (bool, error) {
	return false, nil
}
func (m *switchChunksEngineMock) SearchMetadata(context.Context, *types.SearchMetadataRequest) (*types.SearchMetadataResult, error) {
	return nil, nil
}
func (m *switchChunksEngineMock) IndexDocument(context.Context, string, string, interface{}) error {
	return nil
}
func (m *switchChunksEngineMock) DeleteDocument(context.Context, string, string) error { return nil }
func (m *switchChunksEngineMock) BulkIndex(context.Context, string, []interface{}) (interface{}, error) {
	return nil, nil
}
func (m *switchChunksEngineMock) GetFields([]map[string]interface{}, []string) map[string]map[string]interface{} {
	return nil
}
func (m *switchChunksEngineMock) GetAggregation([]map[string]interface{}, string) []map[string]interface{} {
	return nil
}
func (m *switchChunksEngineMock) GetHighlight([]map[string]interface{}, []string, string) map[string]string {
	return nil
}
func (m *switchChunksEngineMock) RunSQL(context.Context, string, string, []string, string) ([]map[string]interface{}, error) {
	return nil, nil
}
func (m *switchChunksEngineMock) GetChunkIDs([]map[string]interface{}) []string { return nil }
func (m *switchChunksEngineMock) KNNScores(context.Context, []map[string]interface{}, []float64, int) (map[string]interface{}, error) {
	return nil, nil
}
func (m *switchChunksEngineMock) GetScores(map[string]interface{}) map[string]float64 { return nil }
func (m *switchChunksEngineMock) Ping(context.Context) error                          { return nil }
func (m *switchChunksEngineMock) Close() error                                        { return nil }
func (m *switchChunksEngineMock) GetType() string                                     { return "elasticsearch" }
func (m *switchChunksEngineMock) SupportsPageRank() bool                              { return true }
func (m *switchChunksEngineMock) FilterDocIdsByMetaPushdown(context.Context, *gorm.DB, []string, []map[string]interface{}, string) []string {
	return nil
}

func copyMap(in map[string]interface{}) map[string]interface{} {
	out := make(map[string]interface{}, len(in))
	for k, v := range in {
		out[k] = v
	}
	return out
}

func TestChunkServiceParse_RejectsBatchWithRunningIngestionTask(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	insertChunkTestUserTenant(t, "user-1", "tenant-1")
	insertChunkTestKB(t, "kb-1", "tenant-1")
	insertChunkTestDoc(t, "doc-1", "kb-1")
	insertChunkTestDoc(t, "doc-2", "kb-1")
	insertChunkTestIngestionTask(t, "task-2", "user-1", "doc-2", "kb-1", common.RUNNING)

	svc := newParseTestService(t)
	svc.accessibleFunc = func(string, string) bool { return true }
	ctx := t.Context()
	_, _, err := svc.Parse(ctx, "user-1", "kb-1", &service.ParseFileRequest{DocumentIDs: []string{"doc-1", "doc-2"}})
	if err == nil {
		t.Fatal("expected error for RUNNING ingestion task, got nil")
	}
}

func TestChunkServiceParse_CallsStartParseDocumentsWithRerunWithDelete(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	insertChunkTestUserTenant(t, "user-1", "tenant-1")
	insertChunkTestKB(t, "kb-1", "tenant-1")
	insertChunkTestDoc(t, "doc-1", "kb-1")

	svc := newParseTestService(t)
	svc.accessibleFunc = func(string, string) bool { return true }
	var calledOpts document.StartParseOptions
	var calledDocID string
	svc.startParseDocumentsFunc = func(ctx context.Context, doc *entity.Document, kb *entity.Knowledgebase, userID string, opts document.StartParseOptions) error {
		calledOpts = opts
		calledDocID = doc.ID
		return nil
	}
	ctx := t.Context()
	_, code, err := svc.Parse(ctx, "user-1", "kb-1", &service.ParseFileRequest{DocumentIDs: []string{"doc-1"}})
	if err != nil {
		t.Fatalf("Parse: %v", err)
	}
	if code != common.CodeSuccess {
		t.Fatalf("code = %d, want success", code)
	}
	if calledDocID != "doc-1" {
		t.Fatalf("startParseDocuments called with doc %s, want doc-1", calledDocID)
	}
	if !calledOpts.RerunWithDelete {
		t.Fatal("expected RerunWithDelete=true")
	}
}

func TestChunkServiceParse_ReturnsPartialSuccessForDuplicateDocumentIDs(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	userID, datasetID := "user-1", "kb-1"
	insertChunkTestKB(t, datasetID, userID)
	insertChunkTestDoc(t, "doc-1", datasetID)

	svc := newParseTestService(t)
	svc.startParseDocumentsFunc = func(context.Context, *entity.Document, *entity.Knowledgebase, string, document.StartParseOptions) error {
		return nil
	}

	ctx := t.Context()
	result, code, err := svc.Parse(ctx, userID, datasetID, &service.ParseFileRequest{
		DocumentIDs: []string{"doc-1", "doc-1"},
	})
	if err == nil {
		t.Fatal("expected duplicate warning error")
	}
	if code != common.CodeSuccess {
		t.Fatalf("expected CodeSuccess, got %v", code)
	}
	if result["success_count"] != 1 {
		t.Fatalf("expected one successful parse, got %v", result["success_count"])
	}
	errorsValue, ok := result["errors"].([]string)
	if !ok || len(errorsValue) != 1 || !strings.Contains(errorsValue[0], "Duplicate document ids: doc-1") {
		t.Fatalf("unexpected duplicate errors: %#v", result["errors"])
	}
}

func TestStopParsing_CallsCancelIngestionTask(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	insertChunkTestKB(t, "kb-1", "user-1") // owner = user-1, Accessible passes
	insertChunkTestDoc(t, "doc-1", "kb-1")
	// StopParsing now checks the IngestionTask status (not doc.Run).
	insertChunkTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1", common.RUNNING)

	svc := newParseTestService(t)
	var calledDocID string
	svc.cancelIngestionTaskFunc = func(ctx context.Context, doc *entity.Document) error {
		calledDocID = doc.ID
		return nil
	}
	ctx := t.Context()
	_, _, err := svc.StopParsing(ctx, "user-1", "kb-1", service.StopParsingRequest{DocumentIDs: []string{"doc-1"}})
	if err != nil {
		t.Fatalf("StopParsing: %v", err)
	}
	if calledDocID != "doc-1" {
		t.Fatalf("cancelIngestionTaskFunc called with doc %s, want doc-1", calledDocID)
	}
}

func TestStopParsing_RejectsDocumentWithoutIngestionTask(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	insertChunkTestKB(t, "kb-1", "user-1")
	insertChunkTestDoc(t, "doc-1", "kb-1")
	// No IngestionTask — should be rejected (nothing to stop).

	svc := newParseTestService(t)
	var called bool
	svc.cancelIngestionTaskFunc = func(ctx context.Context, doc *entity.Document) error { called = true; return nil }
	ctx := t.Context()
	_, code, err := svc.StopParsing(ctx, "user-1", "kb-1", service.StopParsingRequest{DocumentIDs: []string{"doc-1"}})
	if err == nil {
		t.Fatal("expected error for document without ingestion task")
	}
	if code != common.CodeDataError {
		t.Fatalf("expected CodeDataError, got %v", code)
	}
	if called {
		t.Fatal("cancel should not be called for doc without ingestion task")
	}
}

func TestStopParsing_DoesNotDeleteChunksOrResetCountersAfterCancel(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	insertChunkTestKB(t, "kb-1", "user-1")
	insertChunkTestDoc(t, "doc-1", "kb-1")
	// Pre-set counters on the doc so we can verify they are NOT reset.
	if err := dao.DB.Model(&entity.Document{}).Where("id = ?", "doc-1").Updates(map[string]interface{}{
		"token_num": int64(10),
		"chunk_num": int64(5),
	}).Error; err != nil {
		t.Fatalf("set counters: %v", err)
	}
	insertChunkTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1", common.RUNNING)

	svc := newParseTestService(t)
	svc.cancelIngestionTaskFunc = func(ctx context.Context, doc *entity.Document) error {
		// Simulate CancelDocParse: set doc.run=CANCEL.
		return dao.DB.Model(&entity.Document{}).Where("id = ?", doc.ID).
			Update("run", string(entity.TaskStatusCancel)).Error
	}
	engine := &parseTestDocEngine{chunkStoreExists: true}
	svc.docEngine = engine
	ctx := t.Context()
	_, _, err := svc.StopParsing(ctx, "user-1", "kb-1", service.StopParsingRequest{DocumentIDs: []string{"doc-1"}})
	if err != nil {
		t.Fatalf("StopParsing: %v", err)
	}

	// Counters must NOT be reset.
	doc, err := dao.NewDocumentDAO().GetByID(ctx, db, "doc-1")
	if err != nil {
		t.Fatalf("reload doc: %v", err)
	}
	if doc.TokenNum != 10 {
		t.Fatalf("token_num = %d, want 10 (not reset)", doc.TokenNum)
	}
	if doc.ChunkNum != 5 {
		t.Fatalf("chunk_num = %d, want 5 (not reset)", doc.ChunkNum)
	}

	// Chunks must NOT be deleted during cancel.
	if engine.deleteChunksCalls != 0 {
		t.Fatalf("DeleteChunks called %d times, want 0", engine.deleteChunksCalls)
	}
}
