// repo_robustness_test.go — repository 层鲁棒性测试
// 覆盖：重复主键、唯一索引冲突、大量数据分页正确性、
//
//	SQL 注入防护（参数化查询验证）、软删除后查询、
//	空字符串/极值/特殊字符输入、上下文取消
package repository

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"testing"

	"github.com/user-management-system/internal/domain"
)

// ============================================================
// 1. 唯一索引冲突
// ============================================================

func TestRepo_Robust_DuplicateUsername(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	u1 := &domain.User{Username: "dupuser", Password: "hash", Status: domain.UserStatusActive}
	if err := repo.Create(ctx, u1); err != nil {
		t.Fatalf("第一次创建应成功: %v", err)
	}

	u2 := &domain.User{Username: "dupuser", Password: "hash2", Status: domain.UserStatusActive}
	err := repo.Create(ctx, u2)
	if err == nil {
		t.Error("重复用户名应返回唯一索引冲突错误")
	}
}

func TestRepo_Robust_DuplicateEmail(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	email := "dup@example.com"
	repo.Create(ctx, &domain.User{Username: "user1", Email: domain.StrPtr(email), Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck

	err := repo.Create(ctx, &domain.User{Username: "user2", Email: domain.StrPtr(email), Password: "h", Status: domain.UserStatusActive})
	if err == nil {
		t.Error("重复邮箱应返回唯一索引冲突错误")
	}
}

func TestRepo_Robust_DuplicatePhone(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	phone := "13900000001"
	repo.Create(ctx, &domain.User{Username: "pa", Phone: domain.StrPtr(phone), Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck

	err := repo.Create(ctx, &domain.User{Username: "pb", Phone: domain.StrPtr(phone), Password: "h", Status: domain.UserStatusActive})
	if err == nil {
		t.Error("重复手机号应返回唯一索引冲突错误")
	}
}

func TestRepo_Robust_MultipleNullEmail(t *testing.T) {
	// NULL 不触发唯一约束，多个用户可以都没有邮箱
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	for i := 0; i < 5; i++ {
		err := repo.Create(ctx, &domain.User{
			Username: fmt.Sprintf("nomail%d", i),
			Email:    nil, // NULL
			Password: "hash",
			Status:   domain.UserStatusActive,
		})
		if err != nil {
			t.Fatalf("NULL email 用户%d 创建失败: %v", i, err)
		}
	}
}

// ============================================================
// 2. 查询不存在的记录
// ============================================================

func TestRepo_Robust_GetByID_NotFound(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	_, err := repo.GetByID(context.Background(), 99999)
	if err == nil {
		t.Error("查询不存在的 ID 应返回错误")
	}
}

func TestRepo_Robust_GetByUsername_NotFound(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	_, err := repo.GetByUsername(context.Background(), "ghost")
	if err == nil {
		t.Error("查询不存在的用户名应返回错误")
	}
}

func TestRepo_Robust_GetByEmail_NotFound(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	_, err := repo.GetByEmail(context.Background(), "nope@none.com")
	if err == nil {
		t.Error("查询不存在的邮箱应返回错误")
	}
}

func TestRepo_Robust_GetByPhone_NotFound(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	_, err := repo.GetByPhone(context.Background(), "00000000000")
	if err == nil {
		t.Error("查询不存在的手机号应返回错误")
	}
}

// ============================================================
// 3. 软删除后的查询行为
// ============================================================

func TestRepo_Robust_SoftDelete_HiddenFromGet(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	u := &domain.User{Username: "softdel", Password: "h", Status: domain.UserStatusActive}
	repo.Create(ctx, u) //nolint:errcheck
	id := u.ID

	if err := repo.Delete(ctx, id); err != nil {
		t.Fatalf("Delete: %v", err)
	}

	_, err := repo.GetByID(ctx, id)
	if err == nil {
		t.Error("软删除后 GetByID 应返回错误（记录被隐藏）")
	}
}

func TestRepo_Robust_SoftDelete_HiddenFromList(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	for i := 0; i < 3; i++ {
		repo.Create(ctx, &domain.User{Username: fmt.Sprintf("listdel%d", i), Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck
	}

	users, total, _ := repo.List(ctx, 0, 100)
	initialCount := len(users)
	initialTotal := total

	// 删除第一个
	repo.Delete(ctx, users[0].ID) //nolint:errcheck

	users2, total2, _ := repo.List(ctx, 0, 100)
	if len(users2) != initialCount-1 {
		t.Errorf("删除后 List 应减少 1 条，实际 %d -> %d", initialCount, len(users2))
	}
	if total2 != initialTotal-1 {
		t.Errorf("删除后 total 应减少 1，实际 %d -> %d", initialTotal, total2)
	}
}

func TestRepo_Robust_DeleteNonExistent(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	// 软删除一个不存在的 ID，GORM 通常返回 nil（RowsAffected=0 不报错）
	err := repo.Delete(context.Background(), 99999)
	_ = err // 不 panic 即可
}

// ============================================================
// 4. SQL 注入防护（参数化查询）
// ============================================================

func TestRepo_Robust_SQLInjection_GetByUsername(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	// 先插入一个真实用户
	repo.Create(ctx, &domain.User{Username: "legit", Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck

	// 注入载荷：尝试用 OR '1'='1' 绕过查询
	injections := []string{
		"' OR '1'='1",
		"'; DROP TABLE users; --",
		`" OR "1"="1`,
		"admin'--",
		"legit' UNION SELECT * FROM users --",
	}

	for _, payload := range injections {
		_, err := repo.GetByUsername(ctx, payload)
		if err == nil {
			t.Errorf("SQL 注入载荷 %q 不应返回用户（应返回 not found）", payload)
		}
	}
}

func TestRepo_Robust_SQLInjection_Search(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	repo.Create(ctx, &domain.User{Username: "victim", Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck

	injections := []string{
		"' OR '1'='1",
		"%; SELECT * FROM users; --",
		"victim' UNION SELECT username FROM users --",
	}

	for _, payload := range injections {
		users, _, err := repo.Search(ctx, payload, 0, 100)
		if err != nil {
			continue // 参数化查询报错也可接受
		}
		for _, u := range users {
			if u.Username == "victim" && !strings.Contains(payload, "victim") {
				t.Errorf("SQL 注入载荷 %q 不应返回不匹配的用户", payload)
			}
		}
	}
}

func TestRepo_Robust_SQLInjection_ExistsByUsername(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	repo.Create(ctx, &domain.User{Username: "realuser", Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck

	// 这些载荷不应导致 ExistsByUsername("' OR '1'='1") 返回 true（找到不存在的用户）
	exists, err := repo.ExistsByUsername(ctx, "' OR '1'='1")
	if err != nil {
		t.Logf("ExistsByUsername SQL注入: err=%v (可接受)", err)
		return
	}
	if exists {
		t.Error("SQL 注入载荷在 ExistsByUsername 中不应返回 true")
	}
}

// ============================================================
// 5. 分页边界值
// ============================================================

func TestRepo_Robust_List_ZeroOffset(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()
	for i := 0; i < 5; i++ {
		repo.Create(ctx, &domain.User{Username: fmt.Sprintf("pg%d", i), Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck
	}

	users, total, err := repo.List(ctx, 0, 3)
	if err != nil {
		t.Fatalf("List: %v", err)
	}
	if len(users) != 3 {
		t.Errorf("offset=0, limit=3 应返回 3 条，实际 %d", len(users))
	}
	if total != 5 {
		t.Errorf("total 应为 5，实际 %d", total)
	}
}

func TestRepo_Robust_List_OffsetBeyondTotal(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()
	for i := 0; i < 3; i++ {
		repo.Create(ctx, &domain.User{Username: fmt.Sprintf("ov%d", i), Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck
	}

	users, total, err := repo.List(ctx, 100, 10)
	if err != nil {
		t.Fatalf("List: %v", err)
	}
	if len(users) != 0 {
		t.Errorf("offset 超过总数应返回空列表，实际 %d 条", len(users))
	}
	if total != 3 {
		t.Errorf("total 应为 3，实际 %d", total)
	}
}

func TestRepo_Robust_List_LargeLimit(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()
	for i := 0; i < 10; i++ {
		repo.Create(ctx, &domain.User{Username: fmt.Sprintf("ll%d", i), Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck
	}

	users, _, err := repo.List(ctx, 0, 999999)
	if err != nil {
		t.Fatalf("List with huge limit: %v", err)
	}
	if len(users) != 10 {
		t.Errorf("超大 limit 应返回全部 10 条，实际 %d", len(users))
	}
}

func TestRepo_Robust_List_EmptyDB(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	users, total, err := repo.List(context.Background(), 0, 20)
	if err != nil {
		t.Fatalf("空 DB List 应无错误: %v", err)
	}
	if total != 0 {
		t.Errorf("空 DB total 应为 0，实际 %d", total)
	}
	if len(users) != 0 {
		t.Errorf("空 DB 应返回空列表，实际 %d 条", len(users))
	}
}

// ============================================================
// 6. 搜索边界值
// ============================================================

func TestRepo_Robust_Search_EmptyKeyword(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()
	for i := 0; i < 5; i++ {
		repo.Create(ctx, &domain.User{Username: fmt.Sprintf("sk%d", i), Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck
	}

	users, total, err := repo.Search(ctx, "", 0, 20)
	// 空关键字 → LIKE '%%' 匹配所有；验证不报错
	if err != nil {
		t.Fatalf("空关键字 Search 应无错误: %v", err)
	}
	if total < 5 {
		t.Errorf("空关键字应匹配所有用户（>=5），实际 total=%d，rows=%d", total, len(users))
	}
}

func TestRepo_Robust_Search_SpecialCharsKeyword(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()
	repo.Create(ctx, &domain.User{Username: "normaluser", Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck

	// 含 LIKE 元字符
	for _, kw := range []string{"%", "_", "\\", "%_%", "%%"} {
		_, _, err := repo.Search(ctx, kw, 0, 10)
		if err != nil {
			t.Logf("特殊关键字 %q 搜索出错（可接受）: %v", kw, err)
		}
		// 主要验证不 panic
	}
}

func TestRepo_Robust_Search_VeryLongKeyword(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()
	longKw := strings.Repeat("a", 10000)
	_, _, err := repo.Search(ctx, longKw, 0, 10)
	_ = err // 不应 panic
}

// ============================================================
// 7. 超长字段存储
// ============================================================

func TestRepo_Robust_LongFieldValues(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	u := &domain.User{
		Username: strings.Repeat("x", 45), // varchar(50) 以内
		Password: strings.Repeat("y", 200),
		Nickname: strings.Repeat("n", 45),
		Status:   domain.UserStatusActive,
	}
	err := repo.Create(ctx, u)
	// SQLite 不严格限制 varchar 长度，期望成功；其他数据库可能截断或报错
	if err != nil {
		t.Logf("超长字段创建结果: %v（SQLite 可能允许）", err)
	}
}

// ============================================================
// 8. UpdateLastLogin 特殊 IP
// ============================================================

func TestRepo_Robust_UpdateLastLogin_EmptyIP(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	u := &domain.User{Username: "iptest", Password: "h", Status: domain.UserStatusActive}
	repo.Create(ctx, u) //nolint:errcheck

	// 空 IP 不应报错
	if err := repo.UpdateLastLogin(ctx, u.ID, ""); err != nil {
		t.Errorf("空 IP UpdateLastLogin 应无错误: %v", err)
	}
}

func TestRepo_Robust_UpdateLastLogin_LongIP(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	u := &domain.User{Username: "longiptest", Password: "h", Status: domain.UserStatusActive}
	repo.Create(ctx, u) //nolint:errcheck

	longIP := strings.Repeat("1", 500)
	err := repo.UpdateLastLogin(ctx, u.ID, longIP)
	_ = err // SQLite 宽容，不 panic 即可
}

// ============================================================
// 9. 并发写入安全（SQLite 序列化写入）
// ============================================================

func TestRepo_Robust_ConcurrentCreate_NoDeadlock(t *testing.T) {
	db := openTestDB(t)
	// 启用 WAL 模式可减少锁冲突，这里使用默认设置
	repo := NewUserRepository(db)
	ctx := context.Background()

	const goroutines = 20
	var wg sync.WaitGroup
	var mu sync.Mutex // SQLite 只允许单写，用互斥锁序列化
	errorCount := 0

	for i := 0; i < goroutines; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			mu.Lock()
			defer mu.Unlock()
			err := repo.Create(ctx, &domain.User{
				Username: fmt.Sprintf("concurrent_%d", idx),
				Password: "hash",
				Status:   domain.UserStatusActive,
			})
			if err != nil {
				errorCount++
			}
		}(i)
	}
	wg.Wait()

	if errorCount > 0 {
		t.Errorf("序列化并发写入：%d/%d 次失败", errorCount, goroutines)
	}
}

func TestRepo_Robust_ConcurrentReadWrite_NoDataRace(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()

	// 预先插入数据
	for i := 0; i < 10; i++ {
		repo.Create(ctx, &domain.User{Username: fmt.Sprintf("rw%d", i), Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck
	}

	var wg sync.WaitGroup
	var writeMu sync.Mutex

	for i := 0; i < 30; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			if idx%5 == 0 {
				writeMu.Lock()
				repo.UpdateStatus(ctx, int64(idx%10)+1, domain.UserStatusActive) //nolint:errcheck
				writeMu.Unlock()
			} else {
				repo.GetByID(ctx, int64(idx%10)+1) //nolint:errcheck
			}
		}(i)
	}
	wg.Wait()
	// 无 panic / 数据竞争即通过
}

// ============================================================
// 10. Exists 方法边界
// ============================================================

func TestRepo_Robust_ExistsByUsername_EmptyString(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	// 查询空字符串用户名，不应 panic
	exists, err := repo.ExistsByUsername(context.Background(), "")
	if err != nil {
		t.Logf("ExistsByUsername('') err: %v", err)
	}
	_ = exists
}

func TestRepo_Robust_ExistsByEmail_NilEquivalent(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	// 查询空邮箱
	exists, err := repo.ExistsByEmail(context.Background(), "")
	_ = err
	_ = exists
}

func TestRepo_Robust_ExistsByPhone_SQLInjection(t *testing.T) {
	db := openTestDB(t)
	repo := NewUserRepository(db)
	ctx := context.Background()
	repo.Create(ctx, &domain.User{Username: "phoneuser", Phone: domain.StrPtr("13900000001"), Password: "h", Status: domain.UserStatusActive}) //nolint:errcheck

	exists, err := repo.ExistsByPhone(ctx, "' OR '1'='1")
	if err != nil {
		t.Logf("ExistsByPhone SQL注入 err: %v", err)
		return
	}
	if exists {
		t.Error("SQL 注入载荷在 ExistsByPhone 中不应返回 true")
	}
}
