package tasks

import (
	"fmt"
	"sync"
	"testing"

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

func TestUpdateLocksTaskBeforeProjectValidation(t *testing.T) {
	database, owner, project, task := newTaskLockFixture(t)
	queries := captureTaskQueryOrder(t, database)

	_, err := NewService(database).Update(owner.ID, project.Identity, task.Identity, UpdateTaskInput{Title: task.Title})

	require.NoError(t, err)
	require.NotEmpty(t, *queries)
	require.Equal(t, "task:locked-in-tx", (*queries)[0])
}

func TestShareObjectLocksTaskBeforeObjectValidation(t *testing.T) {
	database, _, project, task := newTaskLockFixture(t)
	note := models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: task.CreatedBy, Title: "Context", Markdown: "Private"}
	require.NoError(t, database.Create(&note).Error)
	queries := captureTaskQueryOrder(t, database)

	err := NewService(database).ShareObject(task.ID, "note", note.ID)

	require.NoError(t, err)
	require.NotEmpty(t, *queries)
	require.Equal(t, "task:locked-in-tx", (*queries)[0])
}

func TestAssigneeOnlySeesExplicitlySharedObjects(t *testing.T) {
	database := newTestDB(t)
	assigneeID := uint(2)
	task := models.SenlinAgentTask{ProjectID: 1, CreatedBy: 1, AssigneeID: &assigneeID, Title: "处理合同"}
	note := models.SenlinAgentNote{ProjectID: 1, CreatedBy: 1, Title: "合同背景", Markdown: "只在共享后可见"}
	require.NoError(t, database.Create(&task).Error)
	require.NoError(t, database.Create(&note).Error)
	service := NewService()

	before, err := service.VisibleLinkedObjects(task.ID, assigneeID)
	require.NoError(t, err)
	require.Empty(t, before)

	require.NoError(t, service.ShareObject(task.ID, "note", note.ID))
	after, err := service.VisibleLinkedObjects(task.ID, assigneeID)
	require.NoError(t, err)
	require.Len(t, after, 1)
	require.Equal(t, "note", after[0].ObjectType)
	require.Equal(t, note.Identity, after[0].ObjectID)
}

func TestShareObjectRejectsUnsupportedType(t *testing.T) {
	newTestDB(t)
	service := NewService()

	err := service.ShareObject(1, "ai_session", 9)

	require.ErrorContains(t, err, "unsupported shared object type")
}

func TestShareObjectRejectsObjectFromAnotherProject(t *testing.T) {
	database := newTestDB(t)
	task := models.SenlinAgentTask{ProjectID: 1, CreatedBy: 1, Title: "Review"}
	note := models.SenlinAgentNote{ProjectID: 2, CreatedBy: 1, Title: "Other project", Markdown: "Private context"}
	require.NoError(t, database.Create(&task).Error)
	require.NoError(t, database.Create(&note).Error)
	service := NewService()

	err := service.ShareObject(task.ID, "note", note.ID)

	require.ErrorContains(t, err, "shared object not found in task project")
}

func TestAssignResolvesIdentityAndTaskDTOReflectsReassignment(t *testing.T) {
	database := newTestDB(t)
	owner := models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}
	first := models.SenlinAgentUser{Email: "first@example.com", DisplayName: "First", PasswordHash: "hash"}
	second := models.SenlinAgentUser{Email: "second@example.com", DisplayName: "Second", PasswordHash: "hash"}
	require.NoError(t, database.Create(&owner).Error)
	require.NoError(t, database.Create(&first).Error)
	require.NoError(t, database.Create(&second).Error)
	project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "Alpha", Identifier: "ALPHA"}
	require.NoError(t, database.Create(&project).Error)
	task := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "安排评审", Status: "open"}
	require.NoError(t, database.Create(&task).Error)
	service := NewService(database)

	require.NoError(t, service.Assign(task.ID, first.Identity))
	assigned, err := service.Update(owner.ID, project.Identity, task.Identity, UpdateTaskInput{Title: task.Title})
	require.NoError(t, err)
	require.Equal(t, first.Identity, *assigned.AssigneeID)

	require.NoError(t, service.Assign(task.ID, second.Identity))
	reassigned, err := service.Update(owner.ID, project.Identity, task.Identity, UpdateTaskInput{Title: task.Title})
	require.NoError(t, err)
	require.Equal(t, second.Identity, *reassigned.AssigneeID)

	var event models.SenlinAgentProjectEvent
	require.NoError(t, database.Where("project_id = ? AND entity_type = ? AND entity_id = ?", project.ID, "task", task.ID).Order("id desc").First(&event).Error)
	require.Equal(t, "task_assigned", event.EventType)
}

func newTestDB(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 newTaskLockFixture(t *testing.T) (*gorm.DB, models.SenlinAgentUser, models.SenlinAgentProject, models.SenlinAgentTask) {
	t.Helper()
	database := newTestDB(t)
	owner := models.SenlinAgentUser{Email: "lock-owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}
	require.NoError(t, database.Create(&owner).Error)
	project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "Lock", Identifier: "LOCK"}
	require.NoError(t, database.Create(&project).Error)
	task := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "Lock me", Status: "open"}
	require.NoError(t, database.Create(&task).Error)
	return database, owner, project, task
}

func captureTaskQueryOrder(t *testing.T, database *gorm.DB) *[]string {
	t.Helper()
	var mutex sync.Mutex
	queries := make([]string, 0, 4)
	callbackName := "test:capture_task_lock_" + t.Name()
	require.NoError(t, database.Callback().Query().Before("gorm:query").Register(callbackName, func(tx *gorm.DB) {
		table := tx.Statement.Table
		if tx.Statement.Schema != nil {
			table = tx.Statement.Schema.Table
		}
		if table != (models.SenlinAgentTask{}).TableName() && table != (models.SenlinAgentProject{}).TableName() {
			return
		}
		entry := "project"
		if table == (models.SenlinAgentTask{}).TableName() {
			entry = "task"
			if _, ok := tx.Statement.Clauses["FOR"]; ok {
				entry += ":locked"
				if _, ok := tx.Statement.ConnPool.(gorm.TxCommitter); ok {
					entry += "-in-tx"
				}
			}
		}
		mutex.Lock()
		queries = append(queries, entry)
		mutex.Unlock()
	}))
	return &queries
}
