package service

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/repository"
	"context"
	"encoding/csv"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

func TestWorksmobileLiveSamanUsersDirectoryProvisioning(t *testing.T) {
	if os.Getenv("WORKSMOBILE_LIVE_SAMAN_PROVISIONING") != "1" {
		t.Skip("live Worksmobile Saman provisioning is disabled")
	}
	ctx := context.Background()
	db, err := gorm.Open(postgres.Open(worksmobileLiveDSN()), &gorm.Config{})
	require.NoError(t, err)

	tenantRepo := repository.NewTenantRepository(db)
	userRepo := repository.NewUserRepository(db)
	userGroupRepo := repository.NewUserGroupRepository(db)
	outboxRepo := repository.NewWorksmobileOutboxRepository(db)
	tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
	client := newWorksmobileLiveClient()
	syncService := NewWorksmobileSyncService(tenantService, userRepo, outboxRepo, client)
	worker := NewWorksmobileRelayWorker(outboxRepo, client)

	root, err := tenantService.GetTenantBySlug(ctx, HanmacFamilyTenantSlug)
	require.NoError(t, err)
	samanTenant, err := tenantService.GetTenantBySlug(ctx, "saman")
	require.NoError(t, err)
	createWorksmobileLiveOrgUnitIfMissing(t, ctx, client, *samanTenant)

	targetEmails := []string{"tester@samaneng.com", "orgadmin@samaneng.com"}
	for _, email := range targetEmails {
		user, err := userRepo.FindByEmail(ctx, email)
		require.NoError(t, err)
		dedupeKey := "user:" + strings.ToLower(WorksmobileUserStatusAction(user.Status)) + ":" + user.ID
		job := findWorksmobileLiveOutboxByDedupe(t, db, dedupeKey)
		if job.Status != domain.WorksmobileOutboxStatusProcessed {
			remote, err := client.FindUser(ctx, user.Email)
			require.NoError(t, err)
			if remote != nil {
				require.NoError(t, outboxRepo.MarkProcessed(ctx, job.ID))
				continue
			}
			item, err := syncService.EnqueueUserSync(ctx, root.ID, user.ID, "", "")
			require.NoError(t, err)
			require.NotEmpty(t, item)
			require.NoError(t, outboxRepo.MarkRetry(ctx, job.ID))
			job = findWorksmobileLiveOutboxByDedupe(t, db, dedupeKey)
			err = worker.processJob(ctx, job)
			require.NoError(t, err)
		}

		processed, err := outboxRepo.FindByID(ctx, job.ID)
		require.NoError(t, err)
		require.Equal(t, domain.WorksmobileOutboxStatusProcessed, processed.Status)
	}

	credentials, err := syncService.ListInitialPasswordCredentials(ctx, root.ID, "")
	require.NoError(t, err)
	seen := map[string]bool{}
	for _, credential := range credentials {
		if credential.Email == "tester@samaneng.com" || credential.Email == "orgadmin@samaneng.com" {
			require.Equal(t, domain.WorksmobileOutboxStatusProcessed, credential.Status)
			require.Len(t, credential.InitialPassword, 16)
			seen[credential.Email] = true
		}
	}
	require.True(t, seen["tester@samaneng.com"])
	require.True(t, seen["orgadmin@samaneng.com"])

	remoteUsers, err := client.ListUsers(ctx)
	require.NoError(t, err)
	remoteByEmail := map[string]WorksmobileRemoteUser{}
	for _, user := range remoteUsers {
		remoteByEmail[user.Email] = user
	}
	require.NotEmpty(t, remoteByEmail["tester@samaneng.com"].ID)
	require.NotEmpty(t, remoteByEmail["orgadmin@samaneng.com"].ID)

	remoteGroups, err := client.ListGroups(ctx)
	require.NoError(t, err)
	foundSamanOrgUnit := false
	for _, group := range remoteGroups {
		if group.ExternalID == samanTenant.ID {
			foundSamanOrgUnit = true
			require.Equal(t, "삼안", group.DisplayName)
		}
	}
	require.True(t, foundSamanOrgUnit)
}

func TestWorksmobileLiveSamanOrgUnitProvisioning(t *testing.T) {
	if os.Getenv("WORKSMOBILE_LIVE_SAMAN_ORGUNIT_PROVISIONING") != "1" {
		t.Skip("live Worksmobile Saman orgunit provisioning is disabled")
	}
	runWorksmobileLiveCompanyOrgUnitProvisioning(t, "saman", "SAMAN_DOMAIN_ID", nil)
}

func TestWorksmobileLiveGPDTDCOrgUnitProvisioning(t *testing.T) {
	if os.Getenv("WORKSMOBILE_LIVE_GPDTDC_ORGUNIT_PROVISIONING") != "1" {
		t.Skip("live Worksmobile GPDTDC orgunit provisioning is disabled")
	}
	runWorksmobileLiveCompanyOrgUnitProvisioning(t, "gpdtdc", "GPDTDC_DOMAIN_ID", map[string]bool{
		"56cd0fd7-b62a-43c0-8db9-74a30468d7cb": true,
	})
}

func TestWorksmobileLiveBaronGroupOrgUnitProvisioning(t *testing.T) {
	if os.Getenv("WORKSMOBILE_LIVE_BARONGROUP_ORGUNIT_PROVISIONING") != "1" {
		t.Skip("live Worksmobile Baron Group orgunit provisioning is disabled")
	}
	runWorksmobileLiveBaronGroupOrgUnitProvisioning(t)
}

func TestWorksmobileLiveSyncHanmacFamilyOrgUnits(t *testing.T) {
	if os.Getenv("WORKSMOBILE_LIVE_SYNC_HANMAC_FAMILY_ORGUNITS") != "1" {
		t.Skip("live Worksmobile Hanmac family orgunit sync is disabled")
	}
	ctx := context.Background()
	db, err := gorm.Open(postgres.Open(worksmobileLiveDSN()), &gorm.Config{})
	require.NoError(t, err)

	tenantRepo := repository.NewTenantRepository(db)
	userRepo := repository.NewUserRepository(db)
	userGroupRepo := repository.NewUserGroupRepository(db)
	tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
	client := newWorksmobileLiveClient()

	root, err := tenantService.GetTenantBySlug(ctx, HanmacFamilyTenantSlug)
	require.NoError(t, err)
	tenants, err := listWorksmobileLiveTenantScope(db, root.ID)
	require.NoError(t, err)
	tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, tenants...))
	targets := worksmobileLiveOrgUnitTargets(t, tenants, tenantByID, *root)
	targetByID := map[string]worksmobileLiveOrgUnitTarget{}
	for _, target := range targets {
		targetByID[target.Tenant.ID] = target
	}

	remoteGroups, err := client.ListGroups(ctx)
	require.NoError(t, err)
	remoteByExternalID, duplicateExternalKeys := worksmobileLiveRemoteByExternalID(remoteGroups)
	require.Empty(t, duplicateExternalKeys, "duplicate Worksmobile external keys")

	for _, target := range sortWorksmobileLiveTargetsTopologically(targets, tenantByID) {
		remote, found := remoteByExternalID[target.Tenant.ID]
		if found && remote.DomainID != target.Payload.DomainID {
			require.Failf(t, "external key is attached to a different Worksmobile domain", "slug=%s external=%s currentDomain=%d expectedDomain=%d", target.Tenant.Slug, target.Tenant.ID, remote.DomainID, target.Payload.DomainID)
		}
		if !found {
			remote, found = findWorksmobileLiveRemoteByPath(remoteGroups, worksmobileLiveRemoteByID(remoteGroups), target.Payload.DomainID, worksmobileLiveTenantOrgPath(target.Tenant, tenantByID))
		}
		if found {
			t.Logf("PATCH orgunit slug=%s id=%s worksID=%s email=%s parent=%s", target.Tenant.Slug, target.Tenant.ID, remote.ID, target.Payload.Email, target.Payload.ParentOrgUnitID)
			require.NoError(t, patchWorksmobileLiveOrgUnit(ctx, client, remote.ID, target.Payload))
		} else {
			t.Logf("CREATE orgunit slug=%s id=%s email=%s parent=%s", target.Tenant.Slug, target.Tenant.ID, target.Payload.Email, target.Payload.ParentOrgUnitID)
			require.NoError(t, client.UpsertOrgUnit(ctx, target.Payload, target.Tenant.Slug))
		}
		time.Sleep(1100 * time.Millisecond)

		remoteGroups, err = client.ListGroups(ctx)
		require.NoError(t, err)
		remoteByExternalID, duplicateExternalKeys = worksmobileLiveRemoteByExternalID(remoteGroups)
		require.Empty(t, duplicateExternalKeys, "duplicate Worksmobile external keys")
	}

	remoteGroups, err = client.ListGroups(ctx)
	require.NoError(t, err)
	remoteByExternalID, duplicateExternalKeys = worksmobileLiveRemoteByExternalID(remoteGroups)
	require.Empty(t, duplicateExternalKeys, "duplicate Worksmobile external keys")
	remoteByID := worksmobileLiveRemoteByID(remoteGroups)
	for _, target := range targets {
		remote, ok := remoteByExternalID[target.Tenant.ID]
		require.True(t, ok, "missing Worksmobile orgunit after sync: %s", target.Tenant.Slug)
		require.Equal(t, target.Payload.DomainID, remote.DomainID, "domain mismatch: %s", target.Tenant.Slug)
		require.Equal(t, target.Tenant.Name, remote.DisplayName, "name mismatch: %s", target.Tenant.Slug)
		require.Equal(t, worksmobileMailLocalPart(target.Payload.Email), remote.MailLocalPart, "email local-part mismatch: %s", target.Tenant.Slug)
		require.Equal(t, strings.ToLower(strings.TrimSpace(target.Payload.Email)), strings.ToLower(strings.TrimSpace(remote.Email)), "email mismatch: %s", target.Tenant.Slug)
		expectedParentID := ""
		if parentExternalKey := strings.TrimPrefix(target.Payload.ParentOrgUnitID, "externalKey:"); parentExternalKey != "" && parentExternalKey != target.Payload.ParentOrgUnitID {
			parentRemote, ok := remoteByExternalID[parentExternalKey]
			require.True(t, ok, "missing Worksmobile parent for %s", target.Tenant.Slug)
			expectedParentID = parentRemote.ID
			parentTarget, ok := targetByID[parentExternalKey]
			require.True(t, ok, "missing Baron parent target for %s", target.Tenant.Slug)
			require.Equal(t, worksmobileMailLocalPart(parentTarget.Payload.Email), parentRemote.MailLocalPart, "parent email local-part mismatch: %s", target.Tenant.Slug)
		}
		require.Equal(t, expectedParentID, remote.ParentID, "parent mismatch: %s", target.Tenant.Slug)
		require.Equal(t, worksmobileLiveTenantOrgPath(target.Tenant, tenantByID), worksmobileLiveRemotePath(remoteByID, remote), "path mismatch: %s", target.Tenant.Slug)
	}

	extraWithExternalKey, extraWithoutExternalKey := worksmobileLiveExtraRemoteGroups(remoteGroups, targetByID)
	t.Logf("SUMMARY synced=%d extraWithExternalKey=%d extraWithoutExternalKey=%d", len(targets), len(extraWithExternalKey), len(extraWithoutExternalKey))
	for _, extra := range extraWithExternalKey {
		t.Logf("EXTRA external=%s name=%s email=%s domain=%d", extra.ExternalID, extra.DisplayName, extra.Email, extra.DomainID)
	}
	for _, extra := range extraWithoutExternalKey {
		t.Logf("EXTRA_NO_EXTERNAL id=%s name=%s email=%s domain=%d", extra.ID, extra.DisplayName, extra.Email, extra.DomainID)
	}
}

func TestWorksmobileLiveInspectGPDTDCOrgUnits(t *testing.T) {
	if os.Getenv("WORKSMOBILE_LIVE_INSPECT_GPDTDC_ORGUNITS") != "1" {
		t.Skip("live Worksmobile GPDTDC orgunit inspection is disabled")
	}
	ctx := context.Background()
	db, err := gorm.Open(postgres.Open(worksmobileLiveDSN()), &gorm.Config{})
	require.NoError(t, err)

	tenantRepo := repository.NewTenantRepository(db)
	userRepo := repository.NewUserRepository(db)
	userGroupRepo := repository.NewUserGroupRepository(db)
	tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
	client := newWorksmobileLiveClient()

	gpdtdcTenant, err := tenantService.GetTenantBySlug(ctx, "gpdtdc")
	require.NoError(t, err)
	tenants, err := listWorksmobileLiveTenantSubtree(db, gpdtdcTenant.ID)
	require.NoError(t, err)
	gpdtdcDomainID, ok := worksmobileDomainIDFromEnv("GPDTDC_DOMAIN_ID")
	require.True(t, ok, "missing GPDTDC_DOMAIN_ID")

	remoteGroups, err := client.ListGroups(ctx)
	require.NoError(t, err)
	remoteByExternalID := map[string]WorksmobileRemoteGroup{}
	remoteByID := map[string]WorksmobileRemoteGroup{}
	gpdtdcRemoteCount := 0
	for _, group := range remoteGroups {
		remoteByID[group.ID] = group
		if group.DomainID == gpdtdcDomainID {
			gpdtdcRemoteCount++
			t.Logf("REMOTE GPDTDC id=%s external=%s name=%s email=%s parent=%s parentName=%s", group.ID, group.ExternalID, group.DisplayName, group.Email, group.ParentID, group.ParentName)
		}
		if group.ExternalID != "" {
			remoteByExternalID[group.ExternalID] = group
		}
	}

	missing := make([]domain.Tenant, 0)
	wrongDomain := make([]WorksmobileRemoteGroup, 0)
	for _, tenant := range tenants {
		if tenant.ID == "56cd0fd7-b62a-43c0-8db9-74a30468d7cb" {
			continue
		}
		remote, ok := remoteByExternalID[tenant.ID]
		if !ok {
			missing = append(missing, tenant)
			continue
		}
		if remote.DomainID != gpdtdcDomainID {
			wrongDomain = append(wrongDomain, remote)
		}
	}
	for _, tenant := range missing {
		t.Logf("MISSING LOCAL id=%s slug=%s name=%s parent=%v", tenant.ID, tenant.Slug, tenant.Name, tenant.ParentID)
	}
	for _, remote := range wrongDomain {
		t.Logf("WRONG DOMAIN external=%s name=%s domainID=%d domainName=%s", remote.ExternalID, remote.DisplayName, remote.DomainID, remote.DomainName)
	}

	remoteUsers, err := client.ListUsers(ctx)
	require.NoError(t, err)
	usersByPrimaryOrg := map[string]int{}
	for _, user := range remoteUsers {
		if user.DomainID != gpdtdcDomainID || user.PrimaryOrgUnitID == "" {
			continue
		}
		usersByPrimaryOrg[user.PrimaryOrgUnitID]++
	}
	for orgID, count := range usersByPrimaryOrg {
		group := remoteByID[orgID]
		t.Logf("USER PRIMARY ORG orgID=%s count=%d external=%s name=%s email=%s", orgID, count, group.ExternalID, group.DisplayName, group.Email)
	}
	t.Logf("SUMMARY localOrganizations=%d remoteGPDTDCDomain=%d matchedExternal=%d missing=%d wrongDomain=%d", len(tenants), gpdtdcRemoteCount, len(remoteByExternalID), len(missing), len(wrongDomain))
	require.Empty(t, missing)
	require.Empty(t, wrongDomain)
}

func TestWorksmobileLiveRecoverGPDTDCOrgUnits(t *testing.T) {
	if os.Getenv("WORKSMOBILE_LIVE_RECOVER_GPDTDC_ORGUNITS") != "1" {
		t.Skip("live Worksmobile GPDTDC orgunit recovery is disabled")
	}
	ctx := context.Background()
	db, err := gorm.Open(postgres.Open(worksmobileLiveDSN()), &gorm.Config{})
	require.NoError(t, err)

	tenantRepo := repository.NewTenantRepository(db)
	userRepo := repository.NewUserRepository(db)
	userGroupRepo := repository.NewUserGroupRepository(db)
	tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
	client := newWorksmobileLiveClient()

	gpdtdcTenant, err := tenantService.GetTenantBySlug(ctx, "gpdtdc")
	require.NoError(t, err)
	tenants, err := listWorksmobileLiveTenantSubtree(db, gpdtdcTenant.ID)
	require.NoError(t, err)
	csvNodes, err := readWorksmobileLiveOrgCSV("../../../adminfront/gpdtdc_org_slugged.csv")
	require.NoError(t, err)
	requireWorksmobileLiveBaronCSVMatch(t, tenants, csvNodes)
	gpdtdcDomainID, ok := worksmobileDomainIDFromEnv("GPDTDC_DOMAIN_ID")
	require.True(t, ok, "missing GPDTDC_DOMAIN_ID")

	remoteGroups, err := client.ListGroups(ctx)
	require.NoError(t, err)
	remoteByID := map[string]WorksmobileRemoteGroup{}
	remoteByExternalID := map[string]WorksmobileRemoteGroup{}
	for _, group := range remoteGroups {
		remoteByID[group.ID] = group
		if group.ExternalID != "" {
			remoteByExternalID[group.ExternalID] = group
		}
	}
	for _, tenant := range tenants {
		current, ok := remoteByExternalID[tenant.ID]
		if !ok || current.DomainID == gpdtdcDomainID {
			continue
		}
		t.Logf("CLEAR conflicting external key id=%s external=%s name=%s email=%s domain=%d", current.ID, current.ExternalID, current.DisplayName, current.Email, current.DomainID)
		if err := client.ClearOrgUnitExternalKey(ctx, current.ID, current.DomainID); err != nil {
			legacyPatch := WorksmobileOrgUnitPatchPayload{
				DomainID:           current.DomainID,
				OrgUnitExternalKey: "legacy-" + current.ID,
			}
			t.Logf("REKEY conflicting external key id=%s replacement=%s error=%v", current.ID, legacyPatch.OrgUnitExternalKey, err)
			require.NoError(t, client.PatchOrgUnit(ctx, current.ID, legacyPatch))
		}
		time.Sleep(1100 * time.Millisecond)
	}
	remoteGroups, err = client.ListGroups(ctx)
	require.NoError(t, err)
	remoteByID = map[string]WorksmobileRemoteGroup{}
	remoteByExternalID = map[string]WorksmobileRemoteGroup{}
	for _, group := range remoteGroups {
		remoteByID[group.ID] = group
		if group.ExternalID != "" {
			remoteByExternalID[group.ExternalID] = group
		}
	}
	for _, tenant := range tenants {
		current, ok := remoteByExternalID[tenant.ID]
		if !ok || current.DomainID == gpdtdcDomainID {
			continue
		}
		legacyPatch := WorksmobileOrgUnitPatchPayload{
			DomainID:           current.DomainID,
			OrgUnitExternalKey: "legacy-" + current.ID,
		}
		t.Logf("REKEY still-conflicting external key id=%s replacement=%s", current.ID, legacyPatch.OrgUnitExternalKey)
		require.NoError(t, client.PatchOrgUnit(ctx, current.ID, legacyPatch))
		time.Sleep(1100 * time.Millisecond)
	}
	remoteGroups, err = client.ListGroups(ctx)
	require.NoError(t, err)
	remoteByID = map[string]WorksmobileRemoteGroup{}
	remoteByExternalID = map[string]WorksmobileRemoteGroup{}
	for _, group := range remoteGroups {
		remoteByID[group.ID] = group
		if group.ExternalID != "" {
			remoteByExternalID[group.ExternalID] = group
		}
	}
	remoteUsers, err := client.ListUsers(ctx)
	require.NoError(t, err)
	usersByPrimaryOrg := map[string]int{}
	for _, user := range remoteUsers {
		if user.DomainID == gpdtdcDomainID && user.PrimaryOrgUnitID != "" {
			usersByPrimaryOrg[user.PrimaryOrgUnitID]++
		}
	}

	type recoveryTarget struct {
		Tenant domain.Tenant
		CSV    worksmobileLiveCSVOrg
		Target WorksmobileRemoteGroup
		Bad    *WorksmobileRemoteGroup
	}
	targets := make([]recoveryTarget, 0)
	badByID := map[string]WorksmobileRemoteGroup{}
	for _, tenant := range tenants {
		node, ok := csvNodes[tenant.Slug]
		if !ok {
			t.Logf("SKIP no CSV node slug=%s name=%s id=%s", tenant.Slug, tenant.Name, tenant.ID)
			continue
		}
		desiredPath := worksmobileLiveCSVPath(csvNodes, tenant.Slug)
		target, found := findWorksmobileLiveRemoteByPath(remoteGroups, remoteByID, gpdtdcDomainID, desiredPath)
		if !found {
			if current, ok := remoteByExternalID[tenant.ID]; ok && current.DomainID == gpdtdcDomainID {
				target = current
				found = true
			}
		}
		require.True(t, found, "missing recovery target slug=%s path=%s", tenant.Slug, desiredPath)
		var bad *WorksmobileRemoteGroup
		if current, ok := remoteByExternalID[tenant.ID]; ok && current.DomainID == gpdtdcDomainID && current.ID != target.ID {
			currentCopy := current
			bad = &currentCopy
			badByID[current.ID] = current
		}
		targets = append(targets, recoveryTarget{Tenant: tenant, CSV: node, Target: target, Bad: bad})
	}

	badIDs := map[string]bool{}
	for id := range badByID {
		collectWorksmobileLiveSubtreeIDs(id, remoteGroups, badIDs)
	}
	badGroups := make([]WorksmobileRemoteGroup, 0, len(badIDs))
	for id := range badIDs {
		group := remoteByID[id]
		if group.ID == "" {
			continue
		}
		require.Zero(t, usersByPrimaryOrg[id], "refusing to delete orgunit with primary users: %s %s", group.DisplayName, id)
		badGroups = append(badGroups, group)
	}
	sort.SliceStable(badGroups, func(i, j int) bool {
		return worksmobileLiveRemoteDepth(remoteByID, badGroups[i]) > worksmobileLiveRemoteDepth(remoteByID, badGroups[j])
	})
	for _, group := range badGroups {
		t.Logf("DELETE duplicate id=%s external=%s name=%s email=%s", group.ID, group.ExternalID, group.DisplayName, group.Email)
		require.NoError(t, client.DeleteOrgUnit(ctx, group.ID))
	}

	for _, target := range targets {
		if badIDs[target.Target.ID] {
			continue
		}
		patch := WorksmobileOrgUnitPatchPayload{
			DomainID:           gpdtdcDomainID,
			Email:              target.CSV.Email,
			OrgUnitName:        target.CSV.Name,
			OrgUnitExternalKey: target.Tenant.ID,
		}
		t.Logf("PATCH existing id=%s external=%s name=%s email=%s", target.Target.ID, target.Tenant.ID, target.CSV.Name, target.CSV.Email)
		require.NoError(t, client.PatchOrgUnit(ctx, target.Target.ID, patch))
		time.Sleep(1100 * time.Millisecond)
	}

	remoteGroups, err = client.ListGroups(ctx)
	require.NoError(t, err)
	remoteByExternalID = map[string]WorksmobileRemoteGroup{}
	for _, group := range remoteGroups {
		if group.ExternalID != "" {
			remoteByExternalID[group.ExternalID] = group
		}
	}
	for _, target := range targets {
		remote, ok := remoteByExternalID[target.Tenant.ID]
		require.True(t, ok, "missing recovered external key for %s", target.Tenant.Slug)
		require.Equal(t, gpdtdcDomainID, remote.DomainID)
		require.Equal(t, target.CSV.Name, remote.DisplayName)
	}
}

func runWorksmobileLiveCompanyOrgUnitProvisioning(t *testing.T, companySlug string, domainIDEnvKey string, skipTenantIDs map[string]bool) {
	t.Helper()
	ctx := context.Background()
	db, err := gorm.Open(postgres.Open(worksmobileLiveDSN()), &gorm.Config{})
	require.NoError(t, err)

	tenantRepo := repository.NewTenantRepository(db)
	userRepo := repository.NewUserRepository(db)
	userGroupRepo := repository.NewUserGroupRepository(db)
	tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
	client := newWorksmobileLiveClient()

	companyTenant, err := tenantService.GetTenantBySlug(ctx, companySlug)
	require.NoError(t, err)
	root, err := tenantService.GetTenantBySlug(ctx, HanmacFamilyTenantSlug)
	require.NoError(t, err)
	tenants, err := listWorksmobileLiveTenantSubtree(db, companyTenant.ID)
	require.NoError(t, err)

	domainID, ok := worksmobileDomainIDFromEnv(domainIDEnvKey)
	require.True(t, ok, "missing %s", domainIDEnvKey)

	tenantByID := worksmobileTenantByID(append([]domain.Tenant{*root}, append([]domain.Tenant{*companyTenant}, tenants...)...))
	for index, tenant := range sortWorksmobileLiveOrgUnitsTopologically(tenants) {
		if skipTenantIDs[tenant.ID] {
			continue
		}
		payload, err := BuildWorksmobileOrgUnitPayloadForDomainTenant(tenant, *companyTenant, root.Config, index+1)
		require.NoError(t, err)
		payload.DomainID = domainID
		if tenant.ParentID != nil && (*tenant.ParentID == companyTenant.ID || skipTenantIDs[*tenant.ParentID]) {
			payload.ParentOrgUnitID = ""
		} else {
			payload = normalizeWorksmobileOrgUnitParent(payload, tenant, tenantByID, root.ID)
		}
		require.NoError(t, client.UpsertOrgUnit(ctx, payload, tenant.Slug), "tenant=%s slug=%s", tenant.Name, tenant.Slug)
		time.Sleep(1100 * time.Millisecond)
	}

	remoteGroups, err := client.ListGroups(ctx)
	require.NoError(t, err)
	remoteByExternalID := map[string]WorksmobileRemoteGroup{}
	for _, group := range remoteGroups {
		if group.ExternalID != "" {
			remoteByExternalID[group.ExternalID] = group
		}
	}
	for _, tenant := range tenants {
		if skipTenantIDs[tenant.ID] {
			continue
		}
		remote, ok := remoteByExternalID[tenant.ID]
		require.True(t, ok, "missing remote orgunit external key for %s %s", tenant.Name, tenant.ID)
		require.Equal(t, tenant.Name, remote.DisplayName)
	}
}

func runWorksmobileLiveBaronGroupOrgUnitProvisioning(t *testing.T) {
	t.Helper()
	ctx := context.Background()
	db, err := gorm.Open(postgres.Open(worksmobileLiveDSN()), &gorm.Config{})
	require.NoError(t, err)

	tenantRepo := repository.NewTenantRepository(db)
	userRepo := repository.NewUserRepository(db)
	userGroupRepo := repository.NewUserGroupRepository(db)
	tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
	client := newWorksmobileLiveClient()

	baronGroup, err := tenantService.GetTenantBySlug(ctx, "baron-group")
	require.NoError(t, err)
	tenants, err := listWorksmobileLiveTenantScope(db, baronGroup.ID)
	require.NoError(t, err)
	domainID, ok := worksmobileDomainIDFromEnv("BARONGROUP_DOMAIN_ID")
	require.True(t, ok, "missing BARONGROUP_DOMAIN_ID")
	mailDomain := getenvDefault("BARONGROUP_MAIL_DOMAIN", getenvDefault("WORKS_DEFAULT_DOMAIN_BARONGROUP", "brsw.kr"))

	tenantByID := worksmobileTenantByID(append([]domain.Tenant{*baronGroup}, tenants...))
	targets := worksmobileLiveBaronGroupOrgUnitTargets(t, tenants, tenantByID, *baronGroup, domainID, mailDomain)
	targetByID := map[string]worksmobileLiveOrgUnitTarget{}
	for _, target := range targets {
		targetByID[target.Tenant.ID] = target
	}

	remoteGroups, err := client.ListGroups(ctx)
	require.NoError(t, err)
	remoteByExternalID, duplicateExternalKeys := worksmobileLiveRemoteByExternalID(remoteGroups)
	require.Empty(t, duplicateExternalKeys, "duplicate Worksmobile external keys")

	for _, target := range sortWorksmobileLiveTargetsTopologically(targets, tenantByID) {
		remote, found := remoteByExternalID[target.Tenant.ID]
		if found && remote.DomainID != target.Payload.DomainID {
			t.Logf("REKEY conflicting external key slug=%s external=%s worksID=%s currentDomain=%d expectedDomain=%d", target.Tenant.Slug, target.Tenant.ID, remote.ID, remote.DomainID, target.Payload.DomainID)
			if err := client.ClearOrgUnitExternalKey(ctx, remote.ID, remote.DomainID); err != nil {
				legacyPatch := WorksmobileOrgUnitPatchPayload{
					DomainID:           remote.DomainID,
					OrgUnitExternalKey: "legacy-" + remote.ID,
				}
				require.NoError(t, client.PatchOrgUnit(ctx, remote.ID, legacyPatch))
			}
			time.Sleep(1100 * time.Millisecond)
			remoteGroups, err = client.ListGroups(ctx)
			require.NoError(t, err)
			remoteByExternalID, duplicateExternalKeys = worksmobileLiveRemoteByExternalID(remoteGroups)
			require.Empty(t, duplicateExternalKeys, "duplicate Worksmobile external keys")
			remote, found = remoteByExternalID[target.Tenant.ID]
			if found && remote.DomainID != target.Payload.DomainID {
				require.Failf(t, "external key is attached to a different Worksmobile domain after rekey", "slug=%s external=%s currentDomain=%d expectedDomain=%d", target.Tenant.Slug, target.Tenant.ID, remote.DomainID, target.Payload.DomainID)
			}
		}
		if !found {
			remote, found = findWorksmobileLiveRemoteByPath(remoteGroups, worksmobileLiveRemoteByID(remoteGroups), target.Payload.DomainID, worksmobileLiveTenantOrgPath(target.Tenant, tenantByID))
		}
		if found {
			t.Logf("PATCH orgunit slug=%s id=%s worksID=%s email=%s parent=%s", target.Tenant.Slug, target.Tenant.ID, remote.ID, target.Payload.Email, target.Payload.ParentOrgUnitID)
			require.NoError(t, patchWorksmobileLiveOrgUnit(ctx, client, remote.ID, target.Payload))
		} else {
			t.Logf("CREATE orgunit slug=%s id=%s email=%s parent=%s", target.Tenant.Slug, target.Tenant.ID, target.Payload.Email, target.Payload.ParentOrgUnitID)
			require.NoError(t, client.UpsertOrgUnit(ctx, target.Payload, target.Tenant.Slug))
		}
		time.Sleep(1100 * time.Millisecond)

		remoteGroups, err = client.ListGroups(ctx)
		require.NoError(t, err)
		remoteByExternalID, duplicateExternalKeys = worksmobileLiveRemoteByExternalID(remoteGroups)
		require.Empty(t, duplicateExternalKeys, "duplicate Worksmobile external keys")
	}

	remoteGroups, err = client.ListGroups(ctx)
	require.NoError(t, err)
	remoteByExternalID, duplicateExternalKeys = worksmobileLiveRemoteByExternalID(remoteGroups)
	require.Empty(t, duplicateExternalKeys, "duplicate Worksmobile external keys")
	remoteByID := worksmobileLiveRemoteByID(remoteGroups)
	for _, target := range targets {
		remote, ok := remoteByExternalID[target.Tenant.ID]
		require.True(t, ok, "missing Worksmobile orgunit after sync: %s", target.Tenant.Slug)
		require.Equal(t, target.Payload.DomainID, remote.DomainID, "domain mismatch: %s", target.Tenant.Slug)
		require.Equal(t, target.Tenant.Name, remote.DisplayName, "name mismatch: %s", target.Tenant.Slug)
		require.Equal(t, worksmobileMailLocalPart(target.Payload.Email), remote.MailLocalPart, "email local-part mismatch: %s", target.Tenant.Slug)
		require.Equal(t, strings.ToLower(strings.TrimSpace(target.Payload.Email)), strings.ToLower(strings.TrimSpace(remote.Email)), "email mismatch: %s", target.Tenant.Slug)
		expectedParentID := ""
		if parentExternalKey := strings.TrimPrefix(target.Payload.ParentOrgUnitID, "externalKey:"); parentExternalKey != "" && parentExternalKey != target.Payload.ParentOrgUnitID {
			parentRemote, ok := remoteByExternalID[parentExternalKey]
			require.True(t, ok, "missing Worksmobile parent for %s", target.Tenant.Slug)
			expectedParentID = parentRemote.ID
			parentTarget, ok := targetByID[parentExternalKey]
			require.True(t, ok, "missing Baron parent target for %s", target.Tenant.Slug)
			require.Equal(t, worksmobileMailLocalPart(parentTarget.Payload.Email), parentRemote.MailLocalPart, "parent email local-part mismatch: %s", target.Tenant.Slug)
		}
		require.Equal(t, expectedParentID, remote.ParentID, "parent mismatch: %s", target.Tenant.Slug)
		require.Equal(t, worksmobileLiveTenantOrgPath(target.Tenant, tenantByID), worksmobileLiveRemotePath(remoteByID, remote), "path mismatch: %s", target.Tenant.Slug)
	}

	t.Logf("SUMMARY synced=%d domainID=%d", len(targets), domainID)
}

func worksmobileLiveBaronGroupOrgUnitTargets(t *testing.T, tenants []domain.Tenant, tenantByID map[string]domain.Tenant, root domain.Tenant, domainID int64, mailDomain string) []worksmobileLiveOrgUnitTarget {
	t.Helper()
	mailDomain = strings.ToLower(strings.TrimSpace(mailDomain))
	require.NotEmpty(t, mailDomain, "baron group mail domain is required")
	targets := make([]worksmobileLiveOrgUnitTarget, 0)
	seenExternalKeys := map[string]string{}
	seenEmails := map[string]string{}
	for index, tenant := range tenants {
		if !isWorksmobileOrgUnitTenant(tenant, tenantByID) || worksmobileLiveSkipOrgUnitTenant(tenant) {
			continue
		}
		payload, err := BuildWorksmobileOrgUnitPayloadForDomainTenant(tenant, root, root.Config, index+1)
		require.NoError(t, err, "payload build failed: %s", tenant.Slug)
		payload = normalizeWorksmobileOrgUnitParent(payload, tenant, tenantByID, root.ID)
		payload.DomainID = domainID
		payload.Email = strings.ToLower(strings.TrimSpace(tenant.Slug)) + "@" + mailDomain
		require.NotEmpty(t, payload.Email, "orgunit email is required: %s", tenant.Slug)
		if owner, exists := seenExternalKeys[payload.OrgUnitExternalKey]; exists {
			require.Failf(t, "duplicate Baron external key", "external=%s owner=%s duplicate=%s", payload.OrgUnitExternalKey, owner, tenant.Slug)
		}
		seenExternalKeys[payload.OrgUnitExternalKey] = tenant.Slug
		normalizedEmail := strings.ToLower(strings.TrimSpace(payload.Email))
		if owner, exists := seenEmails[normalizedEmail]; exists {
			require.Failf(t, "duplicate Baron orgunit email", "email=%s owner=%s duplicate=%s", normalizedEmail, owner, tenant.Slug)
		}
		seenEmails[normalizedEmail] = tenant.Slug
		targets = append(targets, worksmobileLiveOrgUnitTarget{Tenant: tenant, Payload: payload})
	}
	return targets
}

func createWorksmobileLiveOrgUnitIfMissing(t *testing.T, ctx context.Context, client *WorksmobileHTTPClient, tenant domain.Tenant) {
	t.Helper()
	payload, err := BuildWorksmobileOrgUnitPayload(tenant, nil, 1)
	require.NoError(t, err)
	if tenant.ParentID != nil {
		payload.ParentOrgUnitID = ""
	}
	err = client.CreateOrgUnit(ctx, payload)
	if apiErr, ok := err.(WorksmobileHTTPError); ok && apiErr.StatusCode == 409 {
		return
	}
	require.NoError(t, err)
}

func listWorksmobileLiveTenantSubtree(db *gorm.DB, rootID string) ([]domain.Tenant, error) {
	var tenants []domain.Tenant
	err := db.Raw(`
with recursive scope as (
	select id, type, parent_id, name, slug, description, status, config, created_at, updated_at, deleted_at
	from tenants
	where id = ? and deleted_at is null
	union all
	select t.id, t.type, t.parent_id, t.name, t.slug, t.description, t.status, t.config, t.created_at, t.updated_at, t.deleted_at
	from tenants t
	join scope on t.parent_id = scope.id
	where t.deleted_at is null
)
select *
from scope
where type = ?
order by name, slug
`, rootID, domain.TenantTypeOrganization).Scan(&tenants).Error
	return tenants, err
}

func listWorksmobileLiveTenantScope(db *gorm.DB, rootID string) ([]domain.Tenant, error) {
	type tenantIDRow struct {
		ID string
	}
	rows := []tenantIDRow{}
	if err := db.Raw(`
with recursive scope as (
	select id, parent_id, created_at
	from tenants
	where id = ? and deleted_at is null
	union all
	select t.id, t.parent_id, t.created_at
	from tenants t
	join scope on t.parent_id = scope.id
	where t.deleted_at is null
)
select id
from scope
where id <> ?
order by created_at, id
`, rootID, rootID).Scan(&rows).Error; err != nil {
		return nil, err
	}
	ids := make([]string, 0, len(rows))
	order := map[string]int{}
	for index, row := range rows {
		ids = append(ids, row.ID)
		order[row.ID] = index
	}
	if len(ids) == 0 {
		return nil, nil
	}
	tenants := []domain.Tenant{}
	if err := db.Preload("Domains").Where("id IN ?", ids).Find(&tenants).Error; err != nil {
		return nil, err
	}
	sort.SliceStable(tenants, func(i, j int) bool {
		return order[tenants[i].ID] < order[tenants[j].ID]
	})
	return tenants, nil
}

type worksmobileLiveOrgUnitTarget struct {
	Tenant  domain.Tenant
	Payload WorksmobileOrgUnitPayload
}

func worksmobileLiveOrgUnitTargets(t *testing.T, tenants []domain.Tenant, tenantByID map[string]domain.Tenant, root domain.Tenant) []worksmobileLiveOrgUnitTarget {
	t.Helper()
	targets := make([]worksmobileLiveOrgUnitTarget, 0)
	seenExternalKeys := map[string]string{}
	seenEmails := map[string]string{}
	for index, tenant := range tenants {
		if !isWorksmobileOrgUnitTenant(tenant, tenantByID) || worksmobileLiveSkipOrgUnitTenant(tenant) {
			continue
		}
		domainTenant := worksmobileDomainClassificationTenant(tenant, tenantByID)
		payload, err := BuildWorksmobileOrgUnitPayloadForDomainTenant(tenant, domainTenant, root.Config, index+1)
		require.NoError(t, err, "payload build failed: %s", tenant.Slug)
		payload = normalizeWorksmobileOrgUnitParent(payload, tenant, tenantByID, root.ID)
		require.NotEmpty(t, payload.Email, "orgunit email is required: %s", tenant.Slug)
		if owner, exists := seenExternalKeys[payload.OrgUnitExternalKey]; exists {
			require.Failf(t, "duplicate Baron external key", "external=%s owner=%s duplicate=%s", payload.OrgUnitExternalKey, owner, tenant.Slug)
		}
		seenExternalKeys[payload.OrgUnitExternalKey] = tenant.Slug
		normalizedEmail := strings.ToLower(strings.TrimSpace(payload.Email))
		if owner, exists := seenEmails[normalizedEmail]; exists {
			require.Failf(t, "duplicate Baron orgunit email", "email=%s owner=%s duplicate=%s", normalizedEmail, owner, tenant.Slug)
		}
		seenEmails[normalizedEmail] = tenant.Slug
		targets = append(targets, worksmobileLiveOrgUnitTarget{Tenant: tenant, Payload: payload})
	}
	return targets
}

func worksmobileLiveSkipOrgUnitTenant(tenant domain.Tenant) bool {
	slug := strings.ToLower(strings.TrimSpace(tenant.Slug))
	name := strings.TrimSpace(tenant.Name)
	return slug == "nw-admin-gpd" || slug == "su2" || name == "네이버웍스관리용"
}

func sortWorksmobileLiveTargetsTopologically(targets []worksmobileLiveOrgUnitTarget, tenantByID map[string]domain.Tenant) []worksmobileLiveOrgUnitTarget {
	byID := map[string]worksmobileLiveOrgUnitTarget{}
	for _, target := range targets {
		byID[target.Tenant.ID] = target
	}
	remaining := append([]worksmobileLiveOrgUnitTarget(nil), targets...)
	sort.SliceStable(remaining, func(i, j int) bool {
		left := worksmobileLiveTenantOrgPath(remaining[i].Tenant, tenantByID)
		right := worksmobileLiveTenantOrgPath(remaining[j].Tenant, tenantByID)
		if left != right {
			return left < right
		}
		return remaining[i].Tenant.Slug < remaining[j].Tenant.Slug
	})
	done := map[string]bool{}
	result := make([]worksmobileLiveOrgUnitTarget, 0, len(remaining))
	for len(remaining) > 0 {
		progress := false
		next := remaining[:0]
		for _, target := range remaining {
			parentReady := true
			if parentID := strings.TrimPrefix(target.Payload.ParentOrgUnitID, "externalKey:"); parentID != "" && parentID != target.Payload.ParentOrgUnitID {
				_, parentInTargets := byID[parentID]
				parentReady = !parentInTargets || done[parentID]
			}
			if parentReady {
				result = append(result, target)
				done[target.Tenant.ID] = true
				progress = true
				continue
			}
			next = append(next, target)
		}
		if !progress {
			result = append(result, next...)
			break
		}
		remaining = next
	}
	return result
}

func worksmobileLiveTenantOrgPath(tenant domain.Tenant, tenantByID map[string]domain.Tenant) string {
	names := []string{tenant.Name}
	current := tenant
	seen := map[string]bool{tenant.ID: true}
	for current.ParentID != nil && strings.TrimSpace(*current.ParentID) != "" {
		parent, ok := tenantByID[*current.ParentID]
		if !ok || seen[parent.ID] || !isWorksmobileOrgUnitTenant(parent, tenantByID) || worksmobileLiveSkipOrgUnitTenant(parent) {
			break
		}
		seen[parent.ID] = true
		names = append([]string{parent.Name}, names...)
		current = parent
	}
	return strings.Join(names, "/")
}

func worksmobileLiveRemoteByExternalID(groups []WorksmobileRemoteGroup) (map[string]WorksmobileRemoteGroup, []string) {
	result := map[string]WorksmobileRemoteGroup{}
	duplicates := []string{}
	seenDuplicate := map[string]bool{}
	for _, group := range groups {
		if group.ExternalID == "" {
			continue
		}
		if _, exists := result[group.ExternalID]; exists {
			if !seenDuplicate[group.ExternalID] {
				duplicates = append(duplicates, group.ExternalID)
				seenDuplicate[group.ExternalID] = true
			}
			continue
		}
		result[group.ExternalID] = group
	}
	sort.Strings(duplicates)
	return result, duplicates
}

func worksmobileLiveRemoteByID(groups []WorksmobileRemoteGroup) map[string]WorksmobileRemoteGroup {
	result := map[string]WorksmobileRemoteGroup{}
	for _, group := range groups {
		result[group.ID] = group
	}
	return result
}

func patchWorksmobileLiveOrgUnit(ctx context.Context, client *WorksmobileHTTPClient, orgUnitID string, payload WorksmobileOrgUnitPayload) error {
	body := map[string]any{
		"domainId":           payload.DomainID,
		"email":              strings.TrimSpace(payload.Email),
		"orgUnitName":        strings.TrimSpace(payload.OrgUnitName),
		"orgUnitExternalKey": strings.TrimSpace(payload.OrgUnitExternalKey),
		"parentOrgUnitId":    strings.TrimSpace(payload.ParentOrgUnitID),
		"displayOrder":       payload.DisplayOrder,
	}
	return client.sendDirectoryJSON(ctx, http.MethodPatch, "/v1.0/orgunits/"+url.PathEscape(strings.TrimSpace(orgUnitID)), body)
}

func worksmobileLiveExtraRemoteGroups(groups []WorksmobileRemoteGroup, targetByID map[string]worksmobileLiveOrgUnitTarget) ([]WorksmobileRemoteGroup, []WorksmobileRemoteGroup) {
	extraWithExternalKey := []WorksmobileRemoteGroup{}
	extraWithoutExternalKey := []WorksmobileRemoteGroup{}
	for _, group := range groups {
		if group.ExternalID == "" {
			extraWithoutExternalKey = append(extraWithoutExternalKey, group)
			continue
		}
		if _, ok := targetByID[group.ExternalID]; !ok {
			extraWithExternalKey = append(extraWithExternalKey, group)
		}
	}
	sort.SliceStable(extraWithExternalKey, func(i, j int) bool {
		return extraWithExternalKey[i].DisplayName < extraWithExternalKey[j].DisplayName
	})
	sort.SliceStable(extraWithoutExternalKey, func(i, j int) bool {
		return extraWithoutExternalKey[i].DisplayName < extraWithoutExternalKey[j].DisplayName
	})
	return extraWithExternalKey, extraWithoutExternalKey
}

func sortWorksmobileLiveOrgUnitsTopologically(tenants []domain.Tenant) []domain.Tenant {
	remaining := append([]domain.Tenant(nil), tenants...)
	sort.SliceStable(remaining, func(i, j int) bool {
		if remaining[i].Name != remaining[j].Name {
			return remaining[i].Name < remaining[j].Name
		}
		return remaining[i].Slug < remaining[j].Slug
	})
	done := map[string]bool{}
	result := make([]domain.Tenant, 0, len(remaining))
	for len(remaining) > 0 {
		progress := false
		next := remaining[:0]
		for _, tenant := range remaining {
			parentReady := tenant.ParentID == nil || *tenant.ParentID == "" || done[*tenant.ParentID]
			if !parentReady {
				parentInScope := false
				for _, candidate := range remaining {
					if candidate.ID == *tenant.ParentID {
						parentInScope = true
						break
					}
				}
				parentReady = !parentInScope
			}
			if parentReady {
				result = append(result, tenant)
				done[tenant.ID] = true
				progress = true
				continue
			}
			next = append(next, tenant)
		}
		if !progress {
			result = append(result, next...)
			break
		}
		remaining = next
	}
	return result
}

type worksmobileLiveCSVOrg struct {
	Slug       string
	Name       string
	Email      string
	ParentSlug string
}

func readWorksmobileLiveOrgCSV(path string) (map[string]worksmobileLiveCSVOrg, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	reader := csv.NewReader(file)
	reader.FieldsPerRecord = -1
	header, err := reader.Read()
	if err != nil {
		return nil, err
	}
	index := map[string]int{}
	for i, value := range header {
		index[strings.TrimSpace(value)] = i
	}
	result := map[string]worksmobileLiveCSVOrg{}
	for {
		row, err := reader.Read()
		if err == io.EOF {
			return result, nil
		}
		if err != nil {
			return nil, err
		}
		email := csvValue(row, index, "메일링 리스트")
		slug := worksmobileMailLocalPart(email)
		if slug == "" {
			continue
		}
		parentSlug := ""
		parent := csvValue(row, index, "상위 조직")
		if start := strings.LastIndex(parent, "("); start >= 0 && strings.HasSuffix(parent, ")") {
			parentSlug = worksmobileMailLocalPart(parent[start+1 : len(parent)-1])
		}
		result[slug] = worksmobileLiveCSVOrg{
			Slug:       slug,
			Name:       csvValue(row, index, "조직명"),
			Email:      email,
			ParentSlug: parentSlug,
		}
	}
}

func requireWorksmobileLiveBaronCSVMatch(t *testing.T, tenants []domain.Tenant, csvNodes map[string]worksmobileLiveCSVOrg) {
	t.Helper()
	tenantSlugs := map[string]domain.Tenant{}
	for _, tenant := range tenants {
		tenantSlugs[tenant.Slug] = tenant
	}
	missingInBaron := make([]string, 0)
	for slug := range csvNodes {
		if _, ok := tenantSlugs[slug]; !ok {
			missingInBaron = append(missingInBaron, slug)
		}
	}
	missingInCSV := make([]string, 0)
	for _, tenant := range tenants {
		if _, ok := csvNodes[tenant.Slug]; !ok {
			missingInCSV = append(missingInCSV, tenant.Slug)
		}
	}
	sort.Strings(missingInBaron)
	sort.Strings(missingInCSV)
	require.Empty(t, missingInBaron, "CSV slugs missing in Baron")
	require.Empty(t, missingInCSV, "Baron slugs missing in CSV")
}

func csvValue(row []string, index map[string]int, key string) string {
	i, ok := index[key]
	if !ok || i < 0 || i >= len(row) {
		return ""
	}
	return strings.TrimSpace(row[i])
}

func worksmobileLiveCSVPath(nodes map[string]worksmobileLiveCSVOrg, slug string) string {
	node, ok := nodes[slug]
	if !ok {
		return slug
	}
	if node.ParentSlug == "" {
		return node.Name
	}
	parentPath := worksmobileLiveCSVPath(nodes, node.ParentSlug)
	if parentPath == "" {
		return node.Name
	}
	return parentPath + "/" + node.Name
}

func findWorksmobileLiveRemoteByPath(groups []WorksmobileRemoteGroup, byID map[string]WorksmobileRemoteGroup, domainID int64, path string) (WorksmobileRemoteGroup, bool) {
	var fallback WorksmobileRemoteGroup
	found := false
	for _, group := range groups {
		if group.DomainID != domainID {
			continue
		}
		if worksmobileLiveRemotePath(byID, group) != path {
			continue
		}
		if group.ExternalID == "" {
			return group, true
		}
		if !found {
			fallback = group
			found = true
		}
	}
	return fallback, found
}

func worksmobileLiveRemotePath(byID map[string]WorksmobileRemoteGroup, group WorksmobileRemoteGroup) string {
	names := []string{group.DisplayName}
	parentID := strings.TrimSpace(group.ParentID)
	seen := map[string]bool{group.ID: true}
	for parentID != "" && !seen[parentID] {
		parent, ok := byID[parentID]
		if !ok {
			break
		}
		seen[parentID] = true
		if parent.DisplayName != "" {
			names = append([]string{parent.DisplayName}, names...)
		}
		parentID = strings.TrimSpace(parent.ParentID)
	}
	return strings.Join(names, "/")
}

func collectWorksmobileLiveSubtreeIDs(rootID string, groups []WorksmobileRemoteGroup, target map[string]bool) {
	if target[rootID] {
		return
	}
	target[rootID] = true
	for _, group := range groups {
		if group.ParentID == rootID {
			collectWorksmobileLiveSubtreeIDs(group.ID, groups, target)
		}
	}
}

func worksmobileLiveRemoteDepth(byID map[string]WorksmobileRemoteGroup, group WorksmobileRemoteGroup) int {
	depth := 0
	parentID := strings.TrimSpace(group.ParentID)
	seen := map[string]bool{group.ID: true}
	for parentID != "" && !seen[parentID] {
		parent, ok := byID[parentID]
		if !ok {
			break
		}
		depth++
		seen[parentID] = true
		parentID = strings.TrimSpace(parent.ParentID)
	}
	return depth
}

func worksmobileLiveDSN() string {
	host := getenvDefault("DB_HOST", "localhost")
	port := getenvDefault("DB_PORT", "5432")
	user := getenvDefault("DB_USER", "baron")
	password := os.Getenv("DB_PASSWORD")
	name := getenvDefault("DB_NAME", "baron_sso")
	return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Seoul", host, user, password, name, port)
}

func findWorksmobileLiveOutboxByDedupe(t *testing.T, db *gorm.DB, dedupeKey string) domain.WorksmobileOutbox {
	t.Helper()
	var job domain.WorksmobileOutbox
	deadline := time.Now().Add(3 * time.Second)
	for {
		err := db.Where("dedupe_key = ?", dedupeKey).First(&job).Error
		if err == nil {
			return job
		}
		if time.Now().After(deadline) {
			require.NoError(t, err)
		}
		time.Sleep(100 * time.Millisecond)
	}
}
