package search

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/glebarez/sqlite"
	"github.com/stretchr/testify/require"
	"gorm.io/gorm"
	"senlinai-agent/backend/internal/config"
	"senlinai-agent/backend/internal/httpx"
	"senlinai-agent/backend/internal/models"
)

func TestSearchHandlerRejectsBlankQuery(t *testing.T) {
	router, _ := newSearchHandlerTestRouter(t, 1)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/search?q=%20%20", nil)
	req.Header.Set("Authorization", "Bearer test-token")
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusBadRequest, rec.Code)
	var payload httpx.ErrorEnvelope
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
	require.Equal(t, "invalid_query", payload.Error.Code)
	require.Equal(t, "请输入搜索关键词", payload.Error.Message)
}

func TestSearchHandlerReturnsCamelCaseIdentityResults(t *testing.T) {
	database := newSearchHandlerTestDB(t)
	owner := createSearchUser(t, database, "owner@example.com")
	project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "回调平台", Identifier: "CALLBACK"}
	require.NoError(t, database.Create(&project).Error)
	task := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "回调任务", Description: "检查签名", Status: "open"}
	require.NoError(t, database.Create(&task).Error)
	note := models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "接口说明", Markdown: "回调验签流程"}
	require.NoError(t, database.Create(&note).Error)
	router := searchHandlerTestRouter(owner.ID)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/search?q=%E5%9B%9E%E8%B0%83", nil)
	req.Header.Set("Authorization", "Bearer test-token")
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	var payload map[string]any
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
	require.ElementsMatch(t, []string{"items"}, searchMapKeys(payload))
	items, ok := payload["items"].([]any)
	require.True(t, ok)
	require.Len(t, items, 3)

	byType := make(map[string]map[string]any, len(items))
	for _, raw := range items {
		item, ok := raw.(map[string]any)
		require.True(t, ok)
		require.ElementsMatch(t, []string{"id", "type", "title", "projectId", "snippet"}, searchMapKeys(item))
		byType[item["type"].(string)] = item
	}
	require.Equal(t, project.Identity, byType["project"]["id"])
	require.Equal(t, project.Identity, byType["project"]["projectId"])
	require.Equal(t, task.Identity, byType["task"]["id"])
	require.Equal(t, project.Identity, byType["task"]["projectId"])
	require.Equal(t, note.Identity, byType["note"]["id"])
	require.Equal(t, project.Identity, byType["note"]["projectId"])
}

func TestSearchHandlerOnlyReturnsObjectsVisibleToCurrentUser(t *testing.T) {
	database := newSearchHandlerTestDB(t)
	owner := createSearchUser(t, database, "owner@example.com")
	viewer := createSearchUser(t, database, "viewer@example.com")
	project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "保密检索词项目", Identifier: "PRIVATE"}
	require.NoError(t, database.Create(&project).Error)
	assigned := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, AssigneeID: &viewer.ID, Title: "检索词已指派任务", Status: "open"}
	require.NoError(t, database.Create(&assigned).Error)
	privateTask := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "检索词私有任务", Status: "open"}
	require.NoError(t, database.Create(&privateTask).Error)
	sharedNote := models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "检索词共享笔记", Markdown: "已显式分享"}
	require.NoError(t, database.Create(&sharedNote).Error)
	privateNote := models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "检索词私有笔记", Markdown: "不得泄漏"}
	require.NoError(t, database.Create(&privateNote).Error)
	otherProject := models.SenlinAgentProject{OwnerID: owner.ID, Name: "其他项目", Identifier: "OTHER"}
	require.NoError(t, database.Create(&otherProject).Error)
	crossProjectNote := models.SenlinAgentNote{ProjectID: otherProject.ID, CreatedBy: owner.ID, Title: "检索词跨项目笔记", Markdown: "伪造分享也不得泄漏"}
	require.NoError(t, database.Create(&crossProjectNote).Error)
	require.NoError(t, database.Create(&models.SenlinAgentTaskShare{
		TaskID: assigned.ID, ObjectType: "note", ObjectID: sharedNote.ID,
	}).Error)
	// 即使历史数据绕过服务层写入跨项目分享，搜索也必须在读取边界再次校验项目一致性。
	require.NoError(t, database.Create(&models.SenlinAgentTaskShare{
		TaskID: assigned.ID, ObjectType: "note", ObjectID: crossProjectNote.ID,
	}).Error)
	router := searchHandlerTestRouter(viewer.ID)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/search?q=%E6%A3%80%E7%B4%A2%E8%AF%8D", nil)
	req.Header.Set("Authorization", "Bearer test-token")
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	var payload struct {
		Items []struct {
			ID        string `json:"id"`
			Type      string `json:"type"`
			ProjectID string `json:"projectId"`
		} `json:"items"`
	}
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
	require.Len(t, payload.Items, 2)
	require.ElementsMatch(t, []string{assigned.Identity, sharedNote.Identity}, []string{payload.Items[0].ID, payload.Items[1].ID})
	for _, item := range payload.Items {
		require.Equal(t, project.Identity, item.ProjectID)
		require.Contains(t, []string{"task", "note"}, item.Type)
		require.NotEqual(t, privateTask.Identity, item.ID)
		require.NotEqual(t, privateNote.Identity, item.ID)
		require.NotEqual(t, crossProjectNote.Identity, item.ID)
		require.NotEqual(t, project.Identity, item.ID)
	}
}

func newSearchHandlerTestRouter(t *testing.T, userID uint) (http.Handler, *gorm.DB) {
	t.Helper()
	database := newSearchHandlerTestDB(t)
	return searchHandlerTestRouter(userID), database
}

func searchHandlerTestRouter(userID uint) http.Handler {
	return httpx.NewProtectedRouter(
		config.Config{Env: "test"},
		func(string) (uint, error) { return userID, nil },
		NewHandler(NewService()),
	)
}

func newSearchHandlerTestDB(t *testing.T) *gorm.DB {
	t.Helper()
	database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, models.AutoMigrate(database))
	models.DBService = database
	return database
}

func createSearchUser(t *testing.T, database *gorm.DB, email string) models.SenlinAgentUser {
	t.Helper()
	user := models.SenlinAgentUser{Email: email, DisplayName: email, PasswordHash: "hash"}
	require.NoError(t, database.Create(&user).Error)
	return user
}

func searchMapKeys(values map[string]any) []string {
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	return keys
}
