package service

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"testing"

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

	"ragflow/internal/common"
	"ragflow/internal/dao"
	enginetypes "ragflow/internal/engine/types"
	"ragflow/internal/entity"
)

func TestIsMessageDocumentNotFound(t *testing.T) {
	if !isMessageDocumentNotFound(fmt.Errorf("wrapped: %w", enginetypes.ErrDocumentNotFound)) {
		t.Fatal("expected wrapped document-not-found error to be recognized")
	}

	if isMessageDocumentNotFound(errors.New("index does not exist")) {
		t.Fatal("expected unrelated backend error to remain a server error")
	}
}

func TestRequireMemoryAccessReturnsCanceledContext(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	err := ctx.Err()
	if _, gotErr := NewMemoryService().requireMemoryAccess(ctx, "user-1", "memory-1"); !errors.Is(gotErr, err) {
		t.Fatalf("requireMemoryAccess error = %v, want %v", gotErr, err)
	}
}

type memoryMessageDocEngine struct {
	fakeChatDocEngine
	searchReq   *enginetypes.SearchRequest
	searchResp  *enginetypes.SearchResult
	updateCond  map[string]interface{}
	updateValue map[string]interface{}
	updateBase  string
	updateID    string
}

func (e *memoryMessageDocEngine) Search(ctx context.Context, req *enginetypes.SearchRequest) (*enginetypes.SearchResult, error) {
	e.searchReq = req
	if e.searchResp != nil {
		return e.searchResp, nil
	}
	return &enginetypes.SearchResult{}, nil
}

func (e *memoryMessageDocEngine) UpdateChunks(ctx context.Context, condition map[string]interface{}, newValue map[string]interface{}, baseName string, datasetID string) error {
	e.updateCond = condition
	e.updateValue = newValue
	e.updateBase = baseName
	e.updateID = datasetID
	return nil
}

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

func (e *memoryMessageDocEngine) GetType() string { return "memory" }

func setupMemoryMessageTestDB(t *testing.T) {
	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.Memory{},
		&entity.User{},
		&entity.UserTenant{},
		&entity.TenantModelProvider{},
		&entity.TenantModelInstance{},
		&entity.TenantModel{},
	); err != nil {
		t.Fatalf("failed to migrate memory test tables: %v", err)
	}

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

func TestUpdateMemoryTeamMemberCannotChangePermissions(t *testing.T) {
	setupMemoryMessageTestDB(t)

	status := "1"
	if err := dao.DB.Create(&entity.Memory{
		ID:          "mem-team",
		Name:        "Shared memory",
		TenantID:    "owner-1",
		MemoryType:  dao.MemoryTypeRaw,
		StorageType: "table",
		EmbdID:      "embd",
		LLMID:       "llm",
		Permissions: string(entity.TenantPermissionTeam),
		MemorySize:  MemorySizeLimit,
	}).Error; err != nil {
		t.Fatalf("seed memory: %v", err)
	}
	if err := dao.DB.Create(&entity.UserTenant{
		ID:        "ut-team",
		UserID:    "member-1",
		TenantID:  "owner-1",
		Role:      "normal",
		InvitedBy: "owner-1",
		Status:    &status,
	}).Error; err != nil {
		t.Fatalf("seed user tenant: %v", err)
	}

	svc := NewMemoryService()
	samePermission := " TEAM "
	if _, err := svc.UpdateMemory(context.Background(), "member-1", "mem-team", &UpdateMemoryRequest{
		Description: sptr("member edit"),
		Permissions: &samePermission,
	}); err != nil {
		t.Fatalf("UpdateMemory same permission error = %v", err)
	}

	nextPermission := "me"
	if _, err := svc.UpdateMemory(context.Background(), "member-1", "mem-team", &UpdateMemoryRequest{
		Permissions: &nextPermission,
	}); err == nil {
		t.Fatal("UpdateMemory permission change error = nil, want error")
	}
}

func TestUpdateMemoryTeamMemberResolvesModelsAgainstOwnerTenant(t *testing.T) {
	setupMemoryMessageTestDB(t)

	status := "1"
	if err := dao.DB.Create(&entity.Memory{
		ID:          "mem-model",
		Name:        "Shared model memory",
		TenantID:    "owner-1",
		MemoryType:  dao.MemoryTypeRaw,
		StorageType: "table",
		EmbdID:      "old-embd",
		LLMID:       "old-llm",
		Permissions: string(entity.TenantPermissionTeam),
		MemorySize:  MemorySizeLimit,
	}).Error; err != nil {
		t.Fatalf("seed memory: %v", err)
	}
	if err := dao.DB.Create(&entity.UserTenant{
		ID:        "ut-model",
		UserID:    "member-1",
		TenantID:  "owner-1",
		Role:      "normal",
		InvitedBy: "owner-1",
		Status:    &status,
	}).Error; err != nil {
		t.Fatalf("seed user tenant: %v", err)
	}

	for _, row := range []struct {
		providerID string
		tenantID   string
		modelID    string
	}{
		{providerID: "provider-owner", tenantID: "owner-1", modelID: "tenant-llm-owner"},
		{providerID: "provider-member", tenantID: "member-1", modelID: "tenant-llm-member"},
	} {
		if err := dao.DB.Create(&entity.TenantModelProvider{
			ID:           row.providerID,
			ProviderName: "OpenAI",
			TenantID:     row.tenantID,
		}).Error; err != nil {
			t.Fatalf("seed provider %s: %v", row.providerID, err)
		}
		instanceID := row.providerID + "-default"
		if err := dao.DB.Create(&entity.TenantModelInstance{
			ID:           instanceID,
			InstanceName: "default",
			ProviderID:   row.providerID,
			APIKey:       "test-key",
			Status:       "active",
		}).Error; err != nil {
			t.Fatalf("seed instance %s: %v", instanceID, err)
		}
		if err := dao.DB.Create(&entity.TenantModel{
			ID:         row.modelID,
			ModelName:  "gpt-4o",
			ProviderID: row.providerID,
			InstanceID: instanceID,
			ModelType:  int(entity.ModelTypeChat),
			Status:     "active",
		}).Error; err != nil {
			t.Fatalf("seed model %s: %v", row.modelID, err)
		}
	}

	llmID := "gpt-4o@default@OpenAI"
	if _, err := NewMemoryService().UpdateMemory(context.Background(), "member-1", "mem-model", &UpdateMemoryRequest{
		LLMID: &llmID,
	}); err != nil {
		t.Fatalf("UpdateMemory model error = %v", err)
	}

	updated, err := dao.NewMemoryDAO().GetByID(context.Background(), dao.DB, "mem-model")
	if err != nil {
		t.Fatalf("get updated memory: %v", err)
	}
	if updated.TenantLLMID == nil || *updated.TenantLLMID != "tenant-llm-owner" {
		t.Fatalf("tenant_llm_id = %v, want tenant-llm-owner", updated.TenantLLMID)
	}
}

func TestListMemoriesUsesTenantModelIDForDisplayName(t *testing.T) {
	setupMemoryMessageTestDB(t)

	if err := dao.DB.Create(&entity.User{ID: "user-1", Nickname: "Owner"}).Error; err != nil {
		t.Fatalf("seed user: %v", err)
	}
	if err := dao.DB.Create(&entity.TenantModelProvider{
		ID:           "provider-1",
		ProviderName: "OpenAI",
		TenantID:     "user-1",
	}).Error; err != nil {
		t.Fatalf("seed provider: %v", err)
	}
	if err := dao.DB.Create(&entity.TenantModelInstance{
		ID:           "instance-1",
		InstanceName: "default",
		ProviderID:   "provider-1",
		APIKey:       "test-key",
	}).Error; err != nil {
		t.Fatalf("seed instance: %v", err)
	}
	if err := dao.DB.Create(&entity.TenantModel{
		ID:         "tenant-llm-1",
		ModelName:  "gpt-4o",
		ProviderID: "provider-1",
		InstanceID: "instance-1",
		ModelType:  int(entity.ModelTypeChat),
		Status:     "active",
	}).Error; err != nil {
		t.Fatalf("seed chat model: %v", err)
	}
	if err := dao.DB.Create(&entity.TenantModel{
		ID:         "tenant-embd-1",
		ModelName:  "text-embedding-3-small",
		ProviderID: "provider-1",
		InstanceID: "instance-1",
		ModelType:  int(entity.ModelTypeEmbedding),
		Status:     "active",
	}).Error; err != nil {
		t.Fatalf("seed embedding model: %v", err)
	}

	tenantLLMID := "tenant-llm-1"
	tenantEmbdID := "tenant-embd-1"
	if err := dao.DB.Create(&entity.Memory{
		ID:               "mem-with-tenant-models",
		Name:             "With tenant models",
		TenantID:         "user-1",
		MemoryType:       dao.MemoryTypeRaw,
		StorageType:      "table",
		LLMID:            "gpt-4o@OpenAI",
		TenantLLMID:      &tenantLLMID,
		EmbdID:           "text-embedding-3-small@OpenAI",
		TenantEmbdID:     &tenantEmbdID,
		Permissions:      string(TenantPermissionMe),
		ForgettingPolicy: string(ForgettingPolicyFIFO),
	}).Error; err != nil {
		t.Fatalf("seed memory: %v", err)
	}

	ctx := t.Context()
	resp, err := NewMemoryService().ListMemories(ctx, "user-1", []string{"user-1"}, nil, "", "", 1, 10)
	if err != nil {
		t.Fatalf("ListMemories: %v", err)
	}
	if resp.TotalCount != 1 || len(resp.MemoryList) != 1 {
		t.Fatalf("ListMemories returned total=%d len=%d, want 1", resp.TotalCount, len(resp.MemoryList))
	}
	memory := resp.MemoryList[0]
	if got, want := memory["llm_id"], "gpt-4o@default@OpenAI"; got != want {
		t.Fatalf("llm_id = %v, want %v", got, want)
	}
	if got, want := memory["embd_id"], "text-embedding-3-small@default@OpenAI"; got != want {
		t.Fatalf("embd_id = %v, want %v", got, want)
	}
}

func TestListMemoriesFallsBackToRawModelIDWithoutTenantModelID(t *testing.T) {
	setupMemoryMessageTestDB(t)

	if err := dao.DB.Create(&entity.User{ID: "user-1", Nickname: "Owner"}).Error; err != nil {
		t.Fatalf("seed user: %v", err)
	}
	if err := dao.DB.Create(&entity.Memory{
		ID:               "mem-without-tenant-models",
		Name:             "Without tenant models",
		TenantID:         "user-1",
		MemoryType:       dao.MemoryTypeRaw,
		StorageType:      "table",
		LLMID:            "raw-llm",
		EmbdID:           "raw-embd",
		Permissions:      string(TenantPermissionMe),
		ForgettingPolicy: string(ForgettingPolicyFIFO),
	}).Error; err != nil {
		t.Fatalf("seed memory: %v", err)
	}

	ctx := t.Context()
	resp, err := NewMemoryService().ListMemories(ctx, "user-1", []string{"user-1"}, nil, "", "", 1, 10)
	if err != nil {
		t.Fatalf("ListMemories: %v", err)
	}
	if resp.TotalCount != 1 || len(resp.MemoryList) != 1 {
		t.Fatalf("ListMemories returned total=%d len=%d, want 1", resp.TotalCount, len(resp.MemoryList))
	}
	memory := resp.MemoryList[0]
	if got, want := memory["llm_id"], "raw-llm"; got != want {
		t.Fatalf("llm_id = %v, want %v", got, want)
	}
	if got, want := memory["embd_id"], "raw-embd"; got != want {
		t.Fatalf("embd_id = %v, want %v", got, want)
	}
}

func seedMemoryMessages(t *testing.T) {
	t.Helper()

	memories := []*entity.Memory{
		{
			ID:               "mem-owned",
			Name:             "Owned",
			TenantID:         "user-1",
			MemoryType:       dao.MemoryTypeRaw,
			StorageType:      "table",
			EmbdID:           "embd-1",
			LLMID:            "llm-1",
			Permissions:      string(TenantPermissionMe),
			ForgettingPolicy: string(ForgettingPolicyFIFO),
		},
		{
			ID:               "mem-other",
			Name:             "Other",
			TenantID:         "user-2",
			MemoryType:       dao.MemoryTypeRaw,
			StorageType:      "table",
			EmbdID:           "embd-2",
			LLMID:            "llm-2",
			Permissions:      string(TenantPermissionMe),
			ForgettingPolicy: string(ForgettingPolicyFIFO),
		},
	}
	for _, memory := range memories {
		if err := dao.DB.Create(memory).Error; err != nil {
			t.Fatalf("seed memory %s: %v", memory.ID, err)
		}
	}
}

func TestSaveAgentMessageBypassesRequestAccessFilter(t *testing.T) {
	setupMemoryMessageTestDB(t)

	if err := dao.DB.Create(&entity.Memory{
		ID:               "mem-owned",
		Name:             "Owned",
		TenantID:         "user-1",
		MemoryType:       dao.MemoryTypeRaw,
		StorageType:      "table",
		EmbdID:           "embd-1",
		LLMID:            "llm-1",
		Permissions:      string(TenantPermissionMe),
		ForgettingPolicy: string(ForgettingPolicyFIFO),
	}).Error; err != nil {
		t.Fatalf("seed memory: %v", err)
	}

	svc := &MemoryService{memoryDAO: dao.NewMemoryDAO()}
	msg := MemoryMessage{
		AgentID:       "agent-1",
		SessionID:     "session-1",
		UserInput:     "hi",
		AgentResponse: "hello",
	}

	ok, detail, err := svc.AddMessage(context.Background(), "", []string{"mem-owned"}, msg)
	if err != nil {
		t.Fatalf("AddMessage: %v", err)
	}
	if ok || detail != "Memory not found." {
		t.Fatalf("AddMessage with empty current user = (%v, %q), want permission-filtered not found", ok, detail)
	}

	ok, detail, err = svc.saveAgentMessage(context.Background(), []string{"mem-owned"}, msg)
	if err != nil {
		t.Fatalf("saveAgentMessage: %v", err)
	}
	if ok {
		t.Fatal("saveAgentMessage unexpectedly succeeded without a message store")
	}
	if strings.Contains(detail, "Memory not found") {
		t.Fatalf("saveAgentMessage was filtered by request user: %q", detail)
	}
	if !strings.Contains(detail, "message store is not initialized") {
		t.Fatalf("saveAgentMessage detail = %q, want message-store failure after memory lookup", detail)
	}
}

func TestGetMessagesFiltersAccessibleMemoryAndBuildsRecentSearch(t *testing.T) {
	setupMemoryMessageTestDB(t)
	seedMemoryMessages(t)

	docEngine := &memoryMessageDocEngine{
		searchResp: &enginetypes.SearchResult{
			Total: 1,
			Chunks: []map[string]interface{}{
				{
					"message_id":   int64(12),
					"message_type": "raw",
					"memory_id":    "mem-owned",
					"user_id":      "user-1",
					"agent_id":     "agent-1",
					"session_id":   "session-1",
					"valid_at":     float64(123),
					"status":       1,
					"content":      "hello",
					"extra":        "should be dropped",
				},
			},
		},
	}
	svc := &MemoryService{memoryDAO: dao.NewMemoryDAO(), docEngine: docEngine}

	got, code, err := svc.GetMessages(context.Background(), []string{"mem-owned", "mem-other"}, "user-1", "agent-1", "session-1", 3)
	if err != nil {
		t.Fatalf("GetMessages error: %v", err)
	}
	if code != common.CodeSuccess {
		t.Fatalf("code = %v, want %v", code, common.CodeSuccess)
	}
	if len(got) != 1 || got[0]["content"] != "hello" {
		t.Fatalf("unexpected messages: %+v", got)
	}
	if _, ok := got[0]["extra"]; ok {
		t.Fatalf("unexpected non-selected field in response: %+v", got[0])
	}

	req := docEngine.searchReq
	if req == nil {
		t.Fatal("expected doc engine search request")
	}
	if !reflect.DeepEqual(req.IndexNames, []string{"memory_user-1"}) {
		t.Fatalf("IndexNames = %v, want [memory_user-1]", req.IndexNames)
	}
	if len(req.KbIDs) != 0 {
		t.Fatalf("KbIDs = %v, want empty for memory message search", req.KbIDs)
	}
	if !reflect.DeepEqual(req.Filter["memory_id"], []string{"mem-owned"}) {
		t.Fatalf("memory_id filter = %v, want [mem-owned]", req.Filter["memory_id"])
	}
	if req.Filter["agent_id"] != "agent-1" || req.Filter["session_id"] != "session-1" {
		t.Fatalf("unexpected filter: %+v", req.Filter)
	}
	if req.Limit != 3 {
		t.Fatalf("Limit = %d, want 3", req.Limit)
	}
	if req.OrderBy == nil || len(req.OrderBy.Fields) != 1 || req.OrderBy.Fields[0].Field != "valid_at" || req.OrderBy.Fields[0].Type != enginetypes.SortDesc {
		t.Fatalf("unexpected order by: %+v", req.OrderBy)
	}
}

func TestSearchMessageFiltersAccessibleMemoryAndDefaultsStatus(t *testing.T) {
	setupMemoryMessageTestDB(t)
	seedMemoryMessages(t)

	docEngine := &memoryMessageDocEngine{
		searchResp: &enginetypes.SearchResult{
			Total: 1,
			Chunks: []map[string]interface{}{
				{
					"message_id":   int64(13),
					"message_type": "raw",
					"memory_id":    "mem-owned",
					"user_id":      "user-1",
					"agent_id":     "agent-1",
					"session_id":   "session-1",
					"valid_at":     int64(456),
					"status":       1,
					"content":      "matched",
				},
			},
		},
	}
	svc := &MemoryService{memoryDAO: dao.NewMemoryDAO(), docEngine: docEngine}
	filter := map[string]interface{}{
		"memory_id":  []string{"mem-owned", "mem-other"},
		"agent_id":   "agent-1",
		"session_id": "session-1",
		"user_id":    "user-1",
	}
	params := map[string]interface{}{
		"query":                      "",
		"similarity_threshold":       0.2,
		"keywords_similarity_weight": 0.7,
		"top_n":                      5,
	}

	got, code, err := svc.SearchMessage(context.Background(), "user-1", filter, params)
	if err != nil {
		t.Fatalf("SearchMessage error: %v", err)
	}
	if code != common.CodeSuccess {
		t.Fatalf("code = %v, want %v", code, common.CodeSuccess)
	}
	if len(got) != 1 || got[0]["content"] != "matched" {
		t.Fatalf("unexpected search result: %+v", got)
	}

	req := docEngine.searchReq
	if req == nil {
		t.Fatal("expected doc engine search request")
	}
	if !reflect.DeepEqual(req.Filter["memory_id"], []string{"mem-owned"}) {
		t.Fatalf("memory_id filter = %v, want [mem-owned]", req.Filter["memory_id"])
	}
	if req.Filter["status"] != 1 {
		t.Fatalf("status filter = %v, want 1", req.Filter["status"])
	}
	if req.Filter["agent_id"] != "agent-1" || req.Filter["session_id"] != "session-1" || req.Filter["user_id"] != "user-1" {
		t.Fatalf("unexpected filter: %+v", req.Filter)
	}
	if len(req.MatchExprs) != 0 {
		t.Fatalf("empty query should not build match expressions, got %+v", req.MatchExprs)
	}
	if req.Limit != 5 {
		t.Fatalf("Limit = %d, want 5", req.Limit)
	}
}

func TestUpdateMessageUpdatesStatusByMessageDocID(t *testing.T) {
	setupMemoryMessageTestDB(t)
	seedMemoryMessages(t)

	docEngine := &memoryMessageDocEngine{}
	svc := &MemoryService{memoryDAO: dao.NewMemoryDAO(), docEngine: docEngine}

	ok, err := svc.UpdateMessage(context.Background(), "user-1", "mem-owned", 42, true)
	if err != nil {
		t.Fatalf("UpdateMessage error: %v", err)
	}
	if !ok {
		t.Fatal("UpdateMessage returned false")
	}
	if docEngine.updateBase != "memory_user-1" {
		t.Fatalf("baseName = %q, want memory_user-1", docEngine.updateBase)
	}
	if docEngine.updateID != "mem-owned" {
		t.Fatalf("datasetID = %q, want mem-owned", docEngine.updateID)
	}
	if docEngine.updateCond["id"] != "mem-owned_42" {
		t.Fatalf("condition = %+v, want id mem-owned_42", docEngine.updateCond)
	}
	if docEngine.updateValue["status"] != 1 {
		t.Fatalf("status update = %+v, want status 1", docEngine.updateValue)
	}
}
