package repository

import (
	"os"
	"reflect"
	"regexp"
	"sort"
	"strings"
	"testing"
)

func TestDefaultPermissionSeedUsesFinancePermissions(t *testing.T) {
	permissions := map[string]struct{}{}
	for _, permission := range defaultPermissions {
		permissions[permission.Code] = struct{}{}
	}

	for _, code := range []string{"money:view", "money:payment-link:create"} {
		if _, ok := permissions[code]; ok {
			t.Fatalf("legacy money permission %s must not be part of default seed", code)
		}
	}

	for _, code := range []string{
		"payment-temporary-link:create",
		"finance:view",
		"finance-withdrawal:view",
		"finance-withdrawal:audit",
		"operations-withdrawal:view",
		"operations-withdrawal:audit",
		"host-withdrawal:view",
	} {
		if _, ok := permissions[code]; !ok {
			t.Fatalf("finance permission %s missing from default seed", code)
		}
	}
	for _, code := range []string{
		"finance-application:create",
		"finance-application:audit",
		"finance-operation:user-coin-credit",
		"finance-operation:user-coin-debit",
		"finance-operation:user-wallet-credit",
		"finance-operation:user-wallet-debit",
		"finance-operation:coin-seller-coin-credit",
		"finance-operation:coin-seller-coin-debit",
	} {
		if _, ok := permissions[code]; ok {
			t.Fatalf("removed finance application permission %s must not be seeded", code)
		}
	}
}

func TestDefaultRoleFinancePermissionTemplates(t *testing.T) {
	opsPermissions := seedTestPermissionSet(defaultRolePermissionCodes(roleCodeOperationsLead))

	for _, code := range []string{
		"payment-bill:export",
		"host-withdrawal:view",
		"operations-withdrawal:view",
		"operations-withdrawal:audit",
	} {
		if _, ok := opsPermissions[code]; !ok {
			t.Fatalf("ops-admin default permissions missing %s", code)
		}
	}

	for _, code := range []string{
		"finance:view",
		"finance-withdrawal:view",
		"finance-withdrawal:audit",
		"payment-temporary-link:create",
		"payment-third-party:update-rate",
		"payment-third-party:sync-rates",
		"finance-application:create",
		"finance-application:audit",
		"finance-operation:user-coin-credit",
		"finance-operation:user-coin-debit",
		"finance-operation:user-wallet-credit",
		"finance-operation:user-wallet-debit",
		"finance-operation:coin-seller-coin-credit",
		"finance-operation:coin-seller-coin-debit",
	} {
		if _, ok := opsPermissions[code]; ok {
			t.Fatalf("ops-admin must not receive removed permission %s", code)
		}
	}
}

func TestManagedDefaultRolePermissionMatrix(t *testing.T) {
	if len(managedDefaultRoles) != 7 {
		t.Fatalf("managed role count = %d, want 7", len(managedDefaultRoles))
	}
	roleNames := map[string]string{}
	for _, role := range managedDefaultRoles {
		if previous, ok := roleNames[role.Code]; ok {
			t.Fatalf("duplicate role code %s for %s and %s", role.Code, previous, role.Name)
		}
		roleNames[role.Code] = role.Name
	}
	wantNames := map[string]string{
		roleCodePlatformAdmin:        "超级管理员",
		roleCodeOperationsLead:       "运营负责人",
		roleCodeOperationsSpecialist: "运营专员",
		roleCodeProductLead:          "产品负责人",
		roleCodeProductSpecialist:    "产品专员",
		roleCodeFinanceLead:          "财务负责人",
		roleCodeFinanceSpecialist:    "财务专员",
	}
	if !reflect.DeepEqual(roleNames, wantNames) {
		t.Fatalf("managed roles = %#v, want %#v", roleNames, wantNames)
	}

	defined := map[string]struct{}{}
	for _, permission := range defaultPermissions {
		defined[permission.Code] = struct{}{}
	}
	for _, role := range managedDefaultRoles {
		if role.Code == roleCodePlatformAdmin {
			continue
		}
		seen := map[string]struct{}{}
		for _, code := range defaultRolePermissionCodes(role.Code) {
			if _, ok := seen[code]; ok {
				t.Fatalf("role %s contains duplicate permission %s", role.Code, code)
			}
			seen[code] = struct{}{}
			if _, ok := defined[code]; !ok {
				t.Fatalf("role %s references permission %s missing from defaultPermissions", role.Code, code)
			}
		}
	}
}

func TestManagedDefaultRoleModuleBoundaries(t *testing.T) {
	assertRolePermissions(t, roleCodeOperationsLead,
		[]string{"app-config:update", "payment-third-party:sync-methods", "game-catalog:update", "country:update", "external-admin-user:view", "external-admin-user:status", "operations-withdrawal:view", "operations-withdrawal:audit"},
		[]string{"app-version:view", "finance:view", "finance-withdrawal:view", "finance-withdrawal:audit", "payment-third-party:update-rate", "role:view", "external-admin-user:create", "external-admin-user:reset-password", "external-admin-user:permissions"},
	)
	assertRolePermissions(t, roleCodeOperationsSpecialist,
		[]string{"room:update", "game-robot:update", "room-rps-config:update", "payment-bill:export", "external-admin-user:view", "external-admin-user:status", "operations-withdrawal:view", "operations-withdrawal:audit"},
		[]string{"activity:update", "daily-task:update", "game-catalog:update", "self-game:update", "coin-adjustment:create", "finance-withdrawal:view", "finance-withdrawal:audit", "external-admin-user:create", "external-admin-user:reset-password", "external-admin-user:permissions"},
	)
	assertRolePermissions(t, roleCodeProductLead,
		[]string{"app-version:update", "daily-task:update", "game-catalog:update", "resource:update"},
		[]string{"bd:create", "coin-adjustment:create", "country:update", "finance:view"},
	)
	assertRolePermissions(t, roleCodeProductSpecialist,
		[]string{"resource:update", "room-robot:update", "game-catalog:view", "daily-task:view"},
		[]string{"app-config:update", "room:update", "game-catalog:update", "daily-task:update", "bd:view"},
	)

	financeLead := defaultRolePermissionCodes(roleCodeFinanceLead)
	financeSpecialist := defaultRolePermissionCodes(roleCodeFinanceSpecialist)
	if !reflect.DeepEqual(financeLead, financeSpecialist) {
		t.Fatalf("finance lead and specialist permissions differ: lead=%v specialist=%v", financeLead, financeSpecialist)
	}
	assertRolePermissions(t, roleCodeFinanceLead,
		[]string{"finance:view", "payment-bill:view", "payment-bill:export", "finance-withdrawal:audit", "room-rps-order:view"},
		[]string{"operations-withdrawal:view", "operations-withdrawal:audit", "game-catalog:view", "room-rps-config:view", "app-user:view", "daily-task:view"},
	)
}

func TestLuckyGiftPermissionsStayInManagedRoleSync(t *testing.T) {
	assertRolePermissions(t, roleCodeOperationsLead,
		[]string{"lucky-gift:view", "lucky-gift:update", "lucky-gift:experiment", "lucky-gift:pool-credit", "lucky-gift:pool-debit"}, nil,
	)
	assertRolePermissions(t, roleCodeOperationsSpecialist,
		[]string{"lucky-gift:view"}, []string{"lucky-gift:update", "lucky-gift:experiment", "lucky-gift:pool-credit", "lucky-gift:pool-debit"},
	)
	assertRolePermissions(t, roleCodeProductLead,
		[]string{"lucky-gift:view", "lucky-gift:update", "lucky-gift:experiment"}, []string{"lucky-gift:pool-credit", "lucky-gift:pool-debit"},
	)
	assertRolePermissions(t, roleCodeProductSpecialist,
		[]string{"lucky-gift:view"}, []string{"lucky-gift:update", "lucky-gift:experiment", "lucky-gift:pool-credit", "lucky-gift:pool-debit"},
	)
}

func TestRolePermissionMigrationMatchesManagedMatrix(t *testing.T) {
	content, err := os.ReadFile("../../migrations/093_role_permission_matrix.sql")
	if err != nil {
		t.Fatalf("read role permission migration: %v", err)
	}
	sqlText := string(content)
	for _, roleCode := range []string{
		roleCodeOperationsLead,
		roleCodeOperationsSpecialist,
		roleCodeProductLead,
		roleCodeProductSpecialist,
	} {
		pattern := regexp.MustCompile(`(?s)WHERE role\.code = '` + regexp.QuoteMeta(roleCode) + `'\s+AND permission\.code IN \((.*?)\);`)
		matches := pattern.FindStringSubmatch(sqlText)
		if len(matches) != 2 {
			t.Fatalf("migration permission block missing for %s", roleCode)
		}
		assertMigrationPermissionCodes(t, roleCode, matches[1])
	}

	financePattern := regexp.MustCompile(`(?s)WHERE role\.code IN \('finance-lead', 'finance-specialist'\)\s+AND permission\.code IN \((.*?)\);`)
	financeMatches := financePattern.FindStringSubmatch(sqlText)
	if len(financeMatches) != 2 {
		t.Fatal("migration permission block missing for finance roles")
	}
	assertMigrationPermissionCodes(t, roleCodeFinanceLead, financeMatches[1])
	assertMigrationPermissionCodes(t, roleCodeFinanceSpecialist, financeMatches[1])

	correction, err := os.ReadFile("../../migrations/102_user_withdrawal_operations_review.sql")
	if err != nil {
		t.Fatalf("read withdrawal review correction migration: %v", err)
	}
	correctionSQL := string(correction)
	for _, token := range []string{
		"schema_migration_checkpoints",
		"COALESCE(MAX(id), 0)",
		"DEFAULT ''pending''",
		"application.id <= checkpoint.checkpoint_value",
		"application.status IN ('approved', 'rejected')",
		"application.operations_status = 'pending'",
		"ALGORITHM=INPLACE, LOCK=NONE",
		"DELETE role_permission",
		"'ops-admin', 'operations-specialist'",
		"'finance-withdrawal:view', 'finance-withdrawal:audit'",
		"'platform-admin', 'ops-admin', 'operations-specialist'",
		"'operations-withdrawal:view', 'operations-withdrawal:audit'",
	} {
		if !strings.Contains(correctionSQL, token) {
			t.Fatalf("withdrawal review correction migration missing %s", token)
		}
	}
	if strings.Contains(correctionSQL, "DEFAULT ''skipped''") || strings.Contains(correctionSQL, "DEFAULT 'skipped'") {
		t.Fatal("withdrawal operations database default must be pending so old gateway inserts cannot bypass initial review")
	}
}

func TestGiftRecordPermissionMigrationExtendsOperationsReadRoles(t *testing.T) {
	content, err := os.ReadFile("../../migrations/095_gift_record_navigation.sql")
	if err != nil {
		t.Fatalf("read gift record permission migration: %v", err)
	}
	sqlText := string(content)
	for _, token := range []string{
		"'gift-record:view'",
		"'ops-admin'",
		"'operations-specialist'",
		"'product-lead'",
		"'product-specialist'",
	} {
		if !strings.Contains(sqlText, token) {
			t.Fatalf("gift record migration missing %s", token)
		}
	}
}

func TestActivityTemplatePermissionMigrationExtendsManagedRoles(t *testing.T) {
	content, err := os.ReadFile("../../migrations/096_activity_template_navigation.sql")
	if err != nil {
		t.Fatalf("read activity template permission migration: %v", err)
	}
	sqlText := string(content)
	for _, token := range []string{
		"'活动模版'", "'activity-template'", "'/activities/templates'",
		"'platform-admin', 'ops-admin', 'product-lead'",
		"'operations-specialist', 'product-specialist', 'auditor', 'readonly'",
		"'activity-template:view'", "'activity-template:create'", "'activity-template:update'",
		"'activity-template:publish'", "'activity-template:delete'", "'activity-template:data'",
		"'activity-template:export'", "'activity-template:retry'",
	} {
		if !strings.Contains(sqlText, token) {
			t.Fatalf("activity template migration missing %s", token)
		}
	}
}

func TestExternalAdminMigrationSeedsIsolatedPortalAndOperationsMenu(t *testing.T) {
	content, err := os.ReadFile("../../migrations/098_external_admin_portal.sql")
	if err != nil {
		t.Fatalf("read external admin migration: %v", err)
	}
	sqlText := string(content)
	for _, token := range []string{
		"external_admin_accounts", "external_admin_sessions", "external_admin_login_logs", "external_admin_operation_logs",
		"uk_external_admin_accounts_app_username", "uk_external_admin_accounts_app_linked_user",
		"'external-admin-user:view'", "'external-admin-user:create'", "'external-admin-user:status'", "'external-admin-user:reset-password'",
		"'operation-external-admin-users'", "'/operations/external-admin-users'",
		"WHERE role.code = 'platform-admin'", "WHERE role.code IN ('ops-admin', 'operations-specialist')",
	} {
		if !strings.Contains(sqlText, token) {
			t.Fatalf("external admin migration missing %s", token)
		}
	}
	// 创建和重置密码会让操作者控制固定的全能力外管账号；迁移必须把这两项
	// 仅授予平台管理员，运营岗位只保留日常查看和启停能力。
	platformBlock := regexp.MustCompile(`(?s)WHERE role\.code = 'platform-admin'\s+AND permission\.code IN \((.*?)\);`).FindStringSubmatch(sqlText)
	if len(platformBlock) != 2 || !strings.Contains(platformBlock[1], "'external-admin-user:create'") || !strings.Contains(platformBlock[1], "'external-admin-user:reset-password'") {
		t.Fatal("external admin credential permissions must be granted to platform-admin")
	}
	opsBlock := regexp.MustCompile(`(?s)WHERE role\.code IN \('ops-admin', 'operations-specialist'\)\s+AND permission\.code IN \((.*?)\);`).FindStringSubmatch(sqlText)
	if len(opsBlock) != 2 || !strings.Contains(opsBlock[1], "'external-admin-user:view'") || !strings.Contains(opsBlock[1], "'external-admin-user:status'") {
		t.Fatal("external admin operations permissions missing for operations roles")
	}
	if strings.Contains(opsBlock[1], "'external-admin-user:create'") || strings.Contains(opsBlock[1], "'external-admin-user:reset-password'") {
		t.Fatal("operations roles must not receive external admin credential permissions")
	}
}

func TestExternalAdminCredentialDelegationCorrectionMigration(t *testing.T) {
	content, err := os.ReadFile("../../migrations/099_external_admin_credential_delegation.sql")
	if err != nil {
		t.Fatalf("read external admin delegation migration: %v", err)
	}
	sqlText := string(content)
	for _, token := range []string{
		"DELETE role_permission", "admin_role_permissions", "'ops-admin'", "'operations-specialist'",
		"'external-admin-user:create'", "'external-admin-user:reset-password'",
	} {
		if !strings.Contains(sqlText, token) {
			t.Fatalf("external admin delegation migration missing %s", token)
		}
	}
}

func TestExternalAdminPermissionAssignmentMigrationIsIncrementalAndPlatformOnly(t *testing.T) {
	content, err := os.ReadFile("../../migrations/101_external_admin_permission_assignment.sql")
	if err != nil {
		t.Fatalf("read external admin permission assignment migration: %v", err)
	}
	sqlText := string(content)
	for _, token := range []string{
		"'external-admin-user:permissions'", "WHERE role.code = 'platform-admin'",
		"admin_role_permissions", "admin_permissions permission", "ADD COLUMN permission_revision",
		"BIGINT UNSIGNED NOT NULL DEFAULT 1", "ALGORITHM=INPLACE", "LOCK=NONE",
	} {
		if !strings.Contains(sqlText, token) {
			t.Fatalf("external admin permission assignment migration missing %s", token)
		}
	}
	if strings.Contains(sqlText, "'ops-admin'") || strings.Contains(sqlText, "'operations-specialist'") {
		t.Fatal("external permission delegation must not be seeded to operations roles")
	}
	upperSQL := strings.ToUpper(sqlText)
	if strings.Contains(upperSQL, "UPDATE EXTERNAL_ADMIN_ACCOUNTS") || strings.Contains(upperSQL, "DELETE FROM EXTERNAL_ADMIN_ACCOUNTS") {
		t.Fatal("permission revision migration must not rewrite account rows in application SQL")
	}
}

func TestExternalAdminIdentityHierarchyMigrationIsBoundedAndBackfillsManagerDefault(t *testing.T) {
	content, err := os.ReadFile("../../migrations/121_external_admin_identity_hierarchy.sql")
	if err != nil {
		t.Fatalf("read external admin identity migration: %v", err)
	}
	sqlText := string(content)
	for _, token := range []string{
		"ADD COLUMN identity_type", "NOT NULL DEFAULT 'country_manager'",
		"ADD COLUMN parent_account_id", "idx_external_admin_accounts_hierarchy",
		"chk_external_admin_accounts_identity", "'local'", "'country_manager'", "'external_super_admin'",
		"fk_external_admin_accounts_parent", "REFERENCES external_admin_accounts(id)",
	} {
		if !strings.Contains(sqlText, token) {
			t.Fatalf("external admin identity migration missing %s", token)
		}
	}
	upperSQL := strings.ToUpper(sqlText)
	for _, forbidden := range []string{"UPDATE USERS", "JOIN USERS", "UPDATE EXTERNAL_ADMIN_ACCOUNTS", "DELETE FROM EXTERNAL_ADMIN_ACCOUNTS"} {
		if strings.Contains(upperSQL, forbidden) {
			t.Fatalf("identity migration must stay bounded to online account-table DDL: %s", forbidden)
		}
	}
}

func TestLuckyGiftPoolAdjustmentPermissionMigrationRepairsManagedRoles(t *testing.T) {
	content, err := os.ReadFile("../../migrations/098_lucky_gift_pool_adjust_permissions.sql")
	if err != nil {
		t.Fatalf("read lucky gift pool permission migration: %v", err)
	}
	sqlText := string(content)
	for _, token := range []string{
		"'lucky-gift:view'", "'lucky-gift:update'", "'lucky-gift:pool-credit'", "'lucky-gift:pool-debit'",
		"'platform-admin', 'ops-admin'", "'operations-specialist'", "'product-lead'", "'product-specialist'",
	} {
		if !strings.Contains(sqlText, token) {
			t.Fatalf("lucky gift pool permission migration missing %s", token)
		}
	}
}

func TestLuckyGiftExperimentPermissionMigrationGrantsUpdateRoleGroup(t *testing.T) {
	content, err := os.ReadFile("../../migrations/103_lucky_gift_experiment_permission.sql")
	if err != nil {
		t.Fatalf("read lucky gift experiment permission migration: %v", err)
	}
	sqlText := string(content)
	for _, token := range []string{
		"'lucky-gift:experiment'",
		"'platform-admin', 'ops-admin', 'product-lead'",
	} {
		if !strings.Contains(sqlText, token) {
			t.Fatalf("lucky gift experiment permission migration missing %s", token)
		}
	}
	// 实验会发布规则版本，但不是资金动作；不能顺手把奖池调账角色组扩进来。
	for _, token := range []string{"'operations-specialist'", "'product-specialist'", "'lucky-gift:pool-credit'", "'lucky-gift:pool-debit'"} {
		if strings.Contains(sqlText, token) {
			t.Fatalf("lucky gift experiment permission migration must not touch %s", token)
		}
	}
}

func assertMigrationPermissionCodes(t *testing.T, roleCode string, sqlList string) {
	t.Helper()
	quotedCode := regexp.MustCompile(`'([a-z0-9:-]+)'`)
	matches := quotedCode.FindAllStringSubmatch(sqlList, -1)
	actual := make([]string, 0, len(matches))
	for _, match := range matches {
		actual = append(actual, match[1])
	}
	expected := append([]string(nil), defaultRolePermissionCodes(roleCode)...)
	// 093 已在生产执行，不能为了新增页面回写旧迁移；095/096/102 以增量方式补充或收紧页面权限。
	// 这里仍校验 093 的原始精确矩阵，新权限由上面的专项迁移测试锁定。
	expected = stringSetDifference(expected, []string{
		"gift-record:view",
		"activity-template:view", "activity-template:create", "activity-template:update", "activity-template:publish",
		"activity-template:delete", "activity-template:data", "activity-template:export", "activity-template:retry",
		"external-admin-user:view", "external-admin-user:create", "external-admin-user:status", "external-admin-user:reset-password",
		"lucky-gift:view", "lucky-gift:update", "lucky-gift:pool-credit", "lucky-gift:pool-debit", "lucky-gift:experiment",
	})
	if roleCode == roleCodeOperationsLead || roleCode == roleCodeOperationsSpecialist {
		// 093 仍记录旧的财务提现权限；102 才把固定运营岗位切换到独立运营审核权限。
		expected = stringSetDifference(expected, []string{"operations-withdrawal:view", "operations-withdrawal:audit"})
		expected = append(expected, "finance-withdrawal:view", "finance-withdrawal:audit")
	}
	sort.Strings(actual)
	sort.Strings(expected)
	if !reflect.DeepEqual(actual, expected) {
		t.Fatalf("migration permissions for %s differ\nactual-only=%v\nexpected-only=%v", roleCode, stringSetDifference(actual, expected), stringSetDifference(expected, actual))
	}
}

func stringSetDifference(left []string, right []string) []string {
	rightSet := make(map[string]struct{}, len(right))
	for _, item := range right {
		rightSet[item] = struct{}{}
	}
	out := make([]string, 0)
	for _, item := range left {
		if _, ok := rightSet[item]; !ok {
			out = append(out, item)
		}
	}
	return out
}

func assertRolePermissions(t *testing.T, roleCode string, required []string, forbidden []string) {
	t.Helper()
	permissions := seedTestPermissionSet(defaultRolePermissionCodes(roleCode))
	for _, code := range required {
		if _, ok := permissions[code]; !ok {
			t.Errorf("role %s missing required permission %s", roleCode, code)
		}
	}
	for _, code := range forbidden {
		if _, ok := permissions[code]; ok {
			t.Errorf("role %s must not have permission %s", roleCode, code)
		}
	}
}

func seedTestPermissionSet(codes []string) map[string]struct{} {
	out := make(map[string]struct{}, len(codes))
	for _, code := range codes {
		out[code] = struct{}{}
	}
	return out
}
