package seed

import (
	"fmt"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/glebarez/sqlite"
	"github.com/stretchr/testify/require"
	"golang.org/x/crypto/bcrypt"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"senlinai-agent/backend/internal/logic/auth"
	"senlinai-agent/backend/internal/logic/projects"
	"senlinai-agent/backend/internal/models"
)

func TestDemoSeedCreatesFrontendWorkspaceData(t *testing.T) {
	database := newTestDB(t)

	result, err := Demo(database, DemoOptions{
		Email:       "demo@senlin.ai",
		DisplayName: "演示用户",
		Password:    "password123",
	})

	require.NoError(t, err)
	require.Equal(t, "demo@senlin.ai", result.User.Email)
	require.Len(t, result.Projects, 4)

	token, err := auth.NewService("test-secret").Login("demo@senlin.ai", "password123")
	require.NoError(t, err)
	require.NotEmpty(t, token)

	workspace, err := projects.NewService().Workspace(result.User.ID, result.Projects[0].Identity)
	require.NoError(t, err)
	require.Equal(t, "项目 A1", workspace.Project.Name)
	require.GreaterOrEqual(t, len(workspace.Inbox), 4)
	require.GreaterOrEqual(t, len(workspace.Tasks), 3)
	require.GreaterOrEqual(t, len(workspace.AISessions), 4)
	require.GreaterOrEqual(t, len(workspace.NotesSources), 3)
	require.GreaterOrEqual(t, len(workspace.CronPlans), 3)
	for _, plan := range workspace.CronPlans {
		require.Empty(t, plan.LastResult, "演示数据不得声称计划已执行")
	}
	require.Len(t, workspace.Channels, 8)
}

func TestDemoSeedIsIdempotent(t *testing.T) {
	database := newTestDB(t)

	first, err := Demo(database, DemoOptions{Email: "demo@senlin.ai", DisplayName: "演示用户", Password: "password123"})
	require.NoError(t, err)
	second, err := Demo(database, DemoOptions{Email: "demo@senlin.ai", DisplayName: "演示用户", Password: "password123"})
	require.NoError(t, err)

	require.Equal(t, first.User.ID, second.User.ID)
	require.Len(t, second.Projects, 4)
	var userCount int64
	require.NoError(t, database.Model(&models.SenlinAgentUser{}).Where("email = ?", "demo@senlin.ai").Count(&userCount).Error)
	require.Equal(t, int64(1), userCount)

	var projectCount int64
	require.NoError(t, database.Model(&models.SenlinAgentProject{}).Where("owner_id = ?", first.User.ID).Count(&projectCount).Error)
	require.Equal(t, int64(4), projectCount)
	var distinctIdentifiers int64
	require.NoError(t, database.Model(&models.SenlinAgentProject{}).
		Where("owner_id = ?", first.User.ID).
		Distinct("identifier").
		Count(&distinctIdentifiers).Error)
	require.Equal(t, projectCount, distinctIdentifiers)

	var tags []models.SenlinAgentTag
	require.NoError(t, database.Where("project_id = ?", first.Projects[0].ID).Find(&tags).Error)
	require.Len(t, tags, 4)
	for _, tag := range tags {
		require.Equal(t, first.Projects[0].ID, tag.ProjectID)
		require.NotEmpty(t, tag.Identity)
	}
}

func TestDemoSeedUsesIdentifierAsStableProjectKey(t *testing.T) {
	database := newTestDB(t)

	first, err := Demo(database, DemoOptions{Email: "demo@senlin.ai", DisplayName: "演示用户", Password: "password123"})
	require.NoError(t, err)
	original := first.Projects[0]
	require.NoError(t, database.Model(&original).Update("name", "本地重命名项目").Error)

	second, err := Demo(database, DemoOptions{Email: "demo@senlin.ai", DisplayName: "演示用户", Password: "password123"})

	require.NoError(t, err)
	require.Equal(t, original.ID, second.Projects[0].ID)
	require.Equal(t, "A1", second.Projects[0].Identifier)
	require.Equal(t, "本地重命名项目", second.Projects[0].Name)
	var projectCount int64
	require.NoError(t, database.Model(&models.SenlinAgentProject{}).
		Where("owner_id = ? AND identifier = ?", first.User.ID, "A1").
		Count(&projectCount).Error)
	require.Equal(t, int64(1), projectCount)
}

func TestDemoSeedAcceptsAConcurrentWinnerWithoutOverwritingUser(t *testing.T) {
	database := newTestDB(t)
	const email = "seed-race@senlin.ai"
	winnerHash, err := bcrypt.GenerateFromPassword([]byte("winner-password"), bcrypt.MinCost)
	require.NoError(t, err)

	var injected atomic.Bool
	callbackName := "test:inject_concurrent_seed_user"
	require.NoError(t, database.Callback().Create().Before("gorm:create").Register(callbackName, func(tx *gorm.DB) {
		if tx.Statement.Schema == nil || tx.Statement.Schema.Table != (models.SenlinAgentUser{}).TableName() {
			return
		}
		if !injected.CompareAndSwap(false, true) {
			return
		}
		now := time.Now().UTC()
		tx.AddError(tx.Exec(
			"INSERT INTO senlin_agent_users (identity, email, display_name, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
			"00000000-0000-7000-8000-000000000001", email, "并发胜出用户", string(winnerHash), "user", now, now,
		).Error)
	}))
	t.Cleanup(func() { database.Callback().Create().Remove(callbackName) })

	result, err := Demo(database, DemoOptions{Email: email, DisplayName: "不应覆盖", Password: "loser-password"})

	require.NoError(t, err)
	require.True(t, injected.Load())
	require.Equal(t, "并发胜出用户", result.User.DisplayName)
	require.NoError(t, bcrypt.CompareHashAndPassword([]byte(result.User.PasswordHash), []byte("winner-password")))
	require.Error(t, bcrypt.CompareHashAndPassword([]byte(result.User.PasswordHash), []byte("loser-password")))
	var userCount int64
	require.NoError(t, database.Model(&models.SenlinAgentUser{}).Where("email = ?", email).Count(&userCount).Error)
	require.Equal(t, int64(1), userCount)
}

func TestDemoSeedRetriesSerializationFailureOutsideTheTransaction(t *testing.T) {
	for _, sqlState := range []string{"40001", "40P01"} {
		t.Run(sqlState, func(t *testing.T) {
			database := newTestDB(t)
			email := "retry-" + strings.ToLower(sqlState) + "@senlin.ai"
			var injected atomic.Bool
			var sawProjectWrite atomic.Bool
			var failedOwnerIdentity string
			var failedProjectIdentity string
			callbackName := "test:fail_first_seed_project_" + sqlState
			require.NoError(t, database.Callback().Create().After("gorm:create").Register(callbackName, func(tx *gorm.DB) {
				if tx.Error != nil || tx.Statement.Schema == nil || tx.Statement.Schema.Table != (models.SenlinAgentProject{}).TableName() {
					return
				}
				if !injected.CompareAndSwap(false, true) {
					return
				}
				project, ok := tx.Statement.Dest.(*models.SenlinAgentProject)
				if !ok {
					tx.AddError(fmt.Errorf("unexpected project create destination %T", tx.Statement.Dest))
					return
				}
				if tx.RowsAffected == 1 && project.ID != 0 && project.Identity != "" && project.OwnerIdentity != "" {
					sawProjectWrite.Store(true)
					failedOwnerIdentity = project.OwnerIdentity
					failedProjectIdentity = project.Identity
				}
				tx.AddError(testSQLStateError{state: sqlState})
			}))
			t.Cleanup(func() { database.Callback().Create().Remove(callbackName) })

			result, err := Demo(database, DemoOptions{Email: email, DisplayName: "重试用户", Password: "password123"})

			require.NoError(t, err)
			require.True(t, injected.Load())
			require.True(t, sawProjectWrite.Load(), "failed transaction must contain the first inserted project")
			require.NotEqual(t, failedOwnerIdentity, result.User.Identity, "retry must recreate the rolled-back user")
			require.NotEqual(t, failedProjectIdentity, result.Projects[0].Identity, "retry must recreate the rolled-back project")
			require.Equal(t, email, result.User.Email)
			require.Len(t, result.Projects, 4)
			projectIDs := []uint{result.Projects[0].ID, result.Projects[1].ID, result.Projects[2].ID, result.Projects[3].ID}
			requireDemoModelCount(t, database, &models.SenlinAgentUser{}, "email = ?", []any{email}, 1)
			requireDemoModelCount(t, database, &models.SenlinAgentProject{}, "owner_id = ?", []any{result.User.ID}, 4)
			requireDemoModelCount(t, database, &models.SenlinAgentTag{}, "project_id = ?", []any{result.Projects[0].ID}, 4)
			requireDemoModelCount(t, database, &models.SenlinAgentTask{}, "project_id IN ?", []any{projectIDs}, 6)
			requireDemoModelCount(t, database, &models.SenlinAgentInboxItem{}, "project_id IN ?", []any{projectIDs}, 7)
			requireDemoModelCount(t, database, &models.SenlinAgentAISession{}, "project_id = ?", []any{result.Projects[0].ID}, 4)
			requireDemoModelCount(t, database, &models.SenlinAgentNote{}, "project_id = ?", []any{result.Projects[0].ID}, 2)
			requireDemoModelCount(t, database, &models.SenlinAgentSource{}, "project_id = ?", []any{result.Projects[0].ID}, 2)
			requireDemoModelCount(t, database, &models.SenlinAgentCronPlan{}, "project_id = ?", []any{result.Projects[0].ID}, 3)
			requireDemoModelCount(t, database, &models.SenlinAgentProjectChannel{}, "project_id = ?", []any{result.Projects[0].ID}, 2)
		})
	}
}

func requireDemoModelCount(t *testing.T, database *gorm.DB, model any, where string, args []any, expected int64) {
	t.Helper()
	var count int64
	require.NoError(t, database.Model(model).Where(where, args...).Count(&count).Error)
	require.Equal(t, expected, count)
}

type testSQLStateError struct {
	state string
}

func (err testSQLStateError) Error() string {
	return "test PostgreSQL transaction failure " + err.state
}

func (err testSQLStateError) SQLState() string {
	return err.state
}

func TestDemoSeedKeepsSameNamedTagsScopedToTheirProjects(t *testing.T) {
	database := newTestDB(t)

	first, err := Demo(database, DemoOptions{Email: "demo@senlin.ai", DisplayName: "演示用户", Password: "password123"})
	require.NoError(t, err)
	otherTag := models.SenlinAgentTag{ProjectID: first.Projects[1].ID, Name: "UI"}
	require.NoError(t, database.Create(&otherTag).Error)

	_, err = Demo(database, DemoOptions{Email: "demo@senlin.ai", DisplayName: "演示用户", Password: "password123"})

	require.NoError(t, err)
	for _, projectID := range []uint{first.Projects[0].ID, first.Projects[1].ID} {
		var count int64
		require.NoError(t, database.Model(&models.SenlinAgentTag{}).
			Where("project_id = ? AND name = ?", projectID, "UI").
			Count(&count).Error)
		require.Equal(t, int64(1), count)
	}

	var taggedTasks []models.SenlinAgentTask
	require.NoError(t, database.Where("project_id = ? AND tag_id IS NOT NULL", first.Projects[0].ID).Find(&taggedTasks).Error)
	require.NotEmpty(t, taggedTasks)
	for _, task := range taggedTasks {
		var tag models.SenlinAgentTag
		require.NoError(t, database.First(&tag, *task.TagID).Error)
		require.Equal(t, task.ProjectID, tag.ProjectID)
	}
}

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{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	require.NoError(t, err)
	require.NoError(t, models.AutoMigrate(database))
	models.DBService = database
	return database
}
