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/storage"
	"reflect"
	"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 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(),
	}

	_, code, err := svc.Parse(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 := dao.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("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.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 }

	_, code, err := svc.Parse("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 }

	for _, req := range []*service.ParseFileRequest{nil, {DocumentIDs: nil}, {DocumentIDs: []string{}}} {
		_, code, err := svc.Parse("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 }

	_, code, err := svc.Parse("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
	}

	_, code, err := svc.Parse("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 := dao.DB.Model(&entity.Document{}).Where("id = ?", "doc-1").Update("run", running).Error; err != nil {
		t.Fatalf("mark doc running: %v", err)
	}

	svc := newParseTestService(t)
	_, code, err := svc.Parse(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 TestParseReturnsServerErrorWhenDeleteChunksFails(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	userID, datasetID := "user-1", "kb-1"
	insertChunkTestKB(t, datasetID, userID)
	insertChunkTestDoc(t, "doc-1", datasetID)

	deleteErr := errors.New("delete chunks failed")
	engine := &parseTestDocEngine{deleteChunksErr: deleteErr}
	svc := newParseTestService(t)
	svc.docEngine = engine

	_, code, err := svc.Parse(userID, datasetID, &service.ParseFileRequest{DocumentIDs: []string{"doc-1"}})
	if !errors.Is(err, deleteErr) {
		t.Fatalf("expected delete chunks error, got %v", err)
	}
	if code != common.CodeServerError {
		t.Fatalf("expected CodeServerError, got %v", code)
	}
	if engine.deleteChunksCalls != 1 {
		t.Fatalf("expected DeleteChunks once, got %d", engine.deleteChunksCalls)
	}
}

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

	deleteErr := errors.New("delete tasks failed")
	svc := newParseTestService(t)
	svc.deleteTasksByDocIDsFunc = func([]string) (int64, error) { return 0, deleteErr }

	_, code, err := svc.Parse(userID, datasetID, &service.ParseFileRequest{DocumentIDs: []string{"doc-1"}})
	if !errors.Is(err, deleteErr) {
		t.Fatalf("expected delete tasks error, got %v", err)
	}
	if code != common.CodeServerError {
		t.Fatalf("expected CodeServerError, got %v", code)
	}
}

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

	storageErr := errors.New("storage address failed")
	svc := newParseTestService(t)
	svc.getDocumentStorageAddressFunc = func(*entity.Document) (string, string, error) {
		return "", "", storageErr
	}

	_, code, err := svc.Parse(userID, datasetID, &service.ParseFileRequest{DocumentIDs: []string{"doc-1"}})
	if !errors.Is(err, storageErr) {
		t.Fatalf("expected storage error, got %v", err)
	}
	if code != common.CodeServerError {
		t.Fatalf("expected CodeServerError, got %v", code)
	}
}

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

	queueErr := errors.New("queue failed")
	svc := newParseTestService(t)
	svc.queueParseTasksFunc = func(*entity.Document, string, string, int64) error {
		return queueErr
	}

	_, code, err := svc.Parse(userID, datasetID, &service.ParseFileRequest{DocumentIDs: []string{"doc-1"}})
	if !errors.Is(err, queueErr) {
		t.Fatalf("expected queue error, got %v", err)
	}
	if code != common.CodeServerError {
		t.Fatalf("expected CodeServerError, got %v", code)
	}
}

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

	beginErr := errors.New("begin parse failed")
	deleteCalls := 0
	svc := newParseTestService(t)
	svc.beginParseDocumentFunc = func(string) error { return beginErr }
	svc.deleteTasksByDocIDsFunc = func([]string) (int64, error) {
		deleteCalls++
		return 1, nil
	}

	_, code, err := svc.Parse(userID, datasetID, &service.ParseFileRequest{DocumentIDs: []string{"doc-1"}})
	if !errors.Is(err, beginErr) {
		t.Fatalf("expected begin error, got %v", err)
	}
	if code != common.CodeServerError {
		t.Fatalf("expected CodeServerError, got %v", code)
	}
	if deleteCalls != 2 {
		t.Fatalf("expected initial delete and cleanup delete, got %d calls", deleteCalls)
	}
}

func TestParseReturnsPartialSuccessForDuplicateDocumentIDs(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)
	result, code, err := svc.Parse(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 TestParseQueuesAndBeginsDocument(t *testing.T) {
	db := setupChunkTestDB(t)
	pushChunkTestDB(t, db)
	userID, datasetID := "user-1", "kb-1"
	insertChunkTestKB(t, datasetID, userID)
	insertChunkTestDoc(t, "doc-1", datasetID)
	insertChunkTestTask(t, "task-1", "doc-1")

	queueCalls := 0
	svc := newParseTestService(t)
	svc.queueParseTasksFunc = func(doc *entity.Document, bucket, objectName string, priority int64) error {
		queueCalls++
		if doc.ID != "doc-1" || bucket != datasetID || objectName != "doc-1.txt" || priority != 0 {
			t.Fatalf("unexpected queue args: doc=%s bucket=%s object=%s priority=%d", doc.ID, bucket, objectName, priority)
		}
		return nil
	}

	result, code, err := svc.Parse(userID, datasetID, &service.ParseFileRequest{DocumentIDs: []string{"doc-1"}})
	if err != nil {
		t.Fatalf("expected parse success, got %v", err)
	}
	if code != common.CodeSuccess {
		t.Fatalf("expected CodeSuccess, got %v", code)
	}
	if result != nil {
		t.Fatalf("expected nil result, got %#v", result)
	}
	if queueCalls != 1 {
		t.Fatalf("expected queue once, got %d", queueCalls)
	}

	var taskCount int64
	if err := dao.DB.Model(&entity.Task{}).Where("doc_id = ?", "doc-1").Count(&taskCount).Error; err != nil {
		t.Fatalf("count tasks: %v", err)
	}
	if taskCount != 0 {
		t.Fatalf("expected old tasks to be deleted, got %d", taskCount)
	}

	doc, err := dao.NewDocumentDAO().GetByID("doc-1")
	if err != nil {
		t.Fatalf("get doc: %v", err)
	}
	if doc.Run == nil || *doc.Run != string(entity.TaskStatusRunning) {
		t.Fatalf("expected document to be running, got %v", doc.Run)
	}
	if doc.ChunkNum != 0 {
		t.Fatalf("expected chunk_num reset to 0, got %d", doc.ChunkNum)
	}
}

func TestAddChunkSuccess(t *testing.T) {
	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(&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) {
	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(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) {
	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(&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(&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(&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) {
	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(&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 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.Knowledgebase{},
		&entity.Document{},
		&entity.Task{},
	); 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(),
		getDocumentStorageAddressFunc: func(doc *entity.Document) (string, string, error) {
			if doc.Location == nil {
				return doc.KbID, "", nil
			}
			return doc.KbID, *doc.Location, nil
		},
		queueParseTasksFunc: func(*entity.Document, string, string, int64) error {
			return nil
		},
	}
}

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 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
	deleteChunksCalls int
}

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, map[string]interface{}, string, string) (int64, error) {
	e.deleteChunksCalls++
	return 0, 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 false, 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 chunkImageStorage struct {
	exists    bool
	oldBinary []byte
	putCalls  int
}

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 }
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 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(string, []models.Message, *models.APIConfig, *models.ChatConfig) (*models.ChatResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) ChatStreamlyWithSender(string, []models.Message, *models.APIConfig, *models.ChatConfig, func(*string, *string) error) error {
	return nil
}
func (d *stubEmbeddingDriver) Embed(*string, []string, *models.APIConfig, *models.EmbeddingConfig) ([]models.EmbeddingData, error) {
	return d.embeddings, d.embedErr
}
func (d *stubEmbeddingDriver) Rerank(*string, string, []string, *models.APIConfig, *models.RerankConfig) (*models.RerankResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) TranscribeAudio(*string, *string, *models.APIConfig, *models.ASRConfig) (*models.ASRResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) TranscribeAudioWithSender(*string, *string, *models.APIConfig, *models.ASRConfig, func(*string, *string) error) error {
	return nil
}
func (d *stubEmbeddingDriver) AudioSpeech(*string, *string, *models.APIConfig, *models.TTSConfig) (*models.TTSResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) AudioSpeechWithSender(*string, *string, *models.APIConfig, *models.TTSConfig, func(*string, *string) error) error {
	return nil
}
func (d *stubEmbeddingDriver) OCRFile(*string, []byte, *string, *models.APIConfig, *models.OCRConfig) (*models.OCRFileResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) ParseFile(*string, []byte, *string, *models.APIConfig, *models.ParseFileConfig) (*models.ParseFileResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) ListModels(*models.APIConfig) ([]models.ListModelResponse, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) Balance(*models.APIConfig) (map[string]interface{}, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) CheckConnection(*models.APIConfig) error { return nil }
func (d *stubEmbeddingDriver) ListTasks(*models.APIConfig) ([]models.ListTaskStatus, error) {
	return nil, nil
}
func (d *stubEmbeddingDriver) ShowTask(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) FilterDocIdsByMetaPushdown(context.Context, []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(),
	}

	if err := svc.SwitchChunks("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) FilterDocIdsByMetaPushdown(context.Context, []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
}
