package service

import (
	"baron-sso-backend/internal/domain"
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func TestWorksmobileHTTPClientCreateUserPostsDirectoryAdminPasswordPayload(t *testing.T) {
	transport := &captureRoundTripper{
		statusCode: http.StatusCreated,
		body:       `{}`,
	}
	client := &WorksmobileHTTPClient{
		BaseURL:        "https://works.example.test",
		DirectoryToken: "directory-token-1",
		SCIMToken:      "scim-token-1",
		HTTPClient:     &http.Client{Transport: transport},
	}
	err := client.CreateUser(context.Background(), WorksmobileUserPayload{
		DomainID:        300285955,
		Email:           "tester@samaneng.com",
		UserExternalKey: "user-1",
		UserName:        WorksmobileUserName{LastName: "Tester"},
		AliasEmails:     []string{"tester.alias@samaneng.com", "tester.alias2@samaneng.com"},
		Locale:          "ko_KR",
		PasswordConfig: WorksmobilePasswordConfig{
			PasswordCreationType: "ADMIN",
			Password:             GenerateWorksmobileInitialPassword(),
		},
		Organizations: []WorksmobileUserOrganization{
			{DomainID: 300285955, Primary: true, OrgUnits: []WorksmobileUserOrgUnit{{OrgUnitID: "externalKey:tenant-saman"}}},
		},
	})

	require.NoError(t, err)
	require.NotNil(t, transport.request)
	require.Equal(t, "/v1.0/users", transport.request.URL.Path)
	require.Equal(t, http.MethodPost, transport.request.Method)
	require.Equal(t, "Bearer directory-token-1", transport.request.Header.Get("Authorization"))

	var payload map[string]any
	require.NoError(t, json.Unmarshal(transport.requestBody, &payload))
	require.Equal(t, "tester@samaneng.com", payload["email"])
	require.Equal(t, "user-1", payload["userExternalKey"])
	require.NotContains(t, payload, "privateEmail")
	require.Equal(t, []any{"tester.alias@samaneng.com", "tester.alias2@samaneng.com"}, payload["aliasEmails"])
	passwordConfig := payload["passwordConfig"].(map[string]any)
	require.Equal(t, "ADMIN", passwordConfig["passwordCreationType"])
	require.Len(t, passwordConfig["password"], 16)
}

func TestNewWorksmobileUserPatchPayloadNormalizesMalformedKoreanCellPhone(t *testing.T) {
	payload := NewWorksmobileUserPatchPayload(WorksmobileUserPayload{
		DomainID:  1001,
		Email:     "phone-canonical@samaneng.com",
		CellPhone: "+82+821062836786",
		UserName:  WorksmobileUserName{LastName: "Phone Canonical User"},
	})

	require.Equal(t, "+821062836786", payload.CellPhone)
}

func TestNewWorksmobileSCIMUserPayloadNormalizesMalformedKoreanCellPhone(t *testing.T) {
	payload := NewWorksmobileSCIMUserPayload(WorksmobileUserPayload{
		Email:     "phone-canonical@samaneng.com",
		CellPhone: "+82+821062836786",
		UserName:  WorksmobileUserName{LastName: "Phone Canonical User"},
	})

	require.Len(t, payload.PhoneNumbers, 1)
	require.Equal(t, "+821062836786", payload.PhoneNumbers[0].Value)
}

func TestWorksmobileHTTPClientUpsertUserPatchesOnCreateConflictWithoutPasswordOrPrivateEmail(t *testing.T) {
	transport := &captureRoundTripper{
		responses: []captureResponse{
			{statusCode: http.StatusConflict, body: `{"code":"ALREADY_EXISTS"}`},
			{statusCode: http.StatusOK, body: `{}`},
		},
	}
	client := &WorksmobileHTTPClient{
		BaseURL:        "https://works.example.test",
		DirectoryToken: "directory-token-1",
		HTTPClient:     &http.Client{Transport: transport},
	}

	err := client.UpsertUser(context.Background(), WorksmobileUserPayload{
		DomainID:        300285955,
		Email:           "tester@samaneng.com",
		UserExternalKey: "user-1",
		UserName:        WorksmobileUserName{LastName: "Tester"},
		PrivateEmail:    "private@example.com",
		PasswordConfig: WorksmobilePasswordConfig{
			PasswordCreationType: "ADMIN",
			Password:             GenerateWorksmobileInitialPassword(),
		},
		Organizations: []WorksmobileUserOrganization{
			{
				DomainID: 300285955,
				Primary:  true,
				OrgUnits: []WorksmobileUserOrgUnit{
					{OrgUnitID: "externalKey:tenant-saman", Primary: true},
				},
			},
		},
	})

	require.NoError(t, err)
	require.Len(t, transport.requests, 2)
	require.Equal(t, http.MethodPost, transport.requests[0].Method)
	require.Equal(t, "/v1.0/users", transport.requests[0].URL.Path)
	require.Equal(t, http.MethodPatch, transport.requests[1].Method)
	require.Equal(t, "/v1.0/users/tester@samaneng.com", transport.requests[1].URL.Path)

	var patchPayload map[string]any
	require.NoError(t, json.Unmarshal(transport.requestBodies[1], &patchPayload))
	require.NotContains(t, patchPayload, "passwordConfig")
	require.NotContains(t, patchPayload, "privateEmail")
	require.Equal(t, "tester@samaneng.com", patchPayload["email"])
	require.Equal(t, "user-1", patchPayload["userExternalKey"])
}

func TestWorksmobileHTTPClientAddUserAliasEmailPostsDirectoryAliasEndpoint(t *testing.T) {
	transport := &captureRoundTripper{
		statusCode: http.StatusCreated,
		body:       `{}`,
	}
	client := &WorksmobileHTTPClient{
		BaseURL:        "https://works.example.test",
		DirectoryToken: "directory-token-1",
		HTTPClient:     &http.Client{Transport: transport},
	}

	err := client.AddUserAliasEmail(context.Background(), "ypshim@samaneng.com", "ypshim@hanmaceng.co.kr")

	require.NoError(t, err)
	require.NotNil(t, transport.request)
	require.Equal(t, http.MethodPost, transport.request.Method)
	require.Equal(t, "/v1.0/users/ypshim@samaneng.com/alias-emails/ypshim@hanmaceng.co.kr", transport.request.URL.Path)
	require.Equal(t, "Bearer directory-token-1", transport.request.Header.Get("Authorization"))
}

func TestWorksmobileHTTPClientResetUserPasswordPatchesPasswordConfig(t *testing.T) {
	transport := &captureRoundTripper{
		statusCode: http.StatusOK,
		body:       `{}`,
	}
	client := &WorksmobileHTTPClient{
		BaseURL:        "https://works.example.test",
		DirectoryToken: "directory-token-1",
		HTTPClient:     &http.Client{Transport: transport},
	}

	err := client.ResetUserPassword(context.Background(), "target@samaneng.com", "Aa1!Aa1!Aa1!Aa1!")

	require.NoError(t, err)
	require.NotNil(t, transport.request)
	require.Equal(t, http.MethodPatch, transport.request.Method)
	require.Equal(t, "/v1.0/users/target@samaneng.com", transport.request.URL.Path)
	var payload map[string]any
	require.NoError(t, json.Unmarshal(transport.requestBody, &payload))
	passwordConfig := payload["passwordConfig"].(map[string]any)
	require.Equal(t, "ADMIN", passwordConfig["passwordCreationType"])
	require.Equal(t, "Aa1!Aa1!Aa1!Aa1!", passwordConfig["password"])
	require.Equal(t, true, passwordConfig["changePasswordAtNextLogin"])
}

func TestWorksmobileHTTPClientCreateUserRequiresDirectoryToken(t *testing.T) {
	client := &WorksmobileHTTPClient{
		BaseURL:    "https://works.example.test",
		SCIMToken:  "scim-token-1",
		HTTPClient: &http.Client{Transport: &captureRoundTripper{statusCode: http.StatusCreated, body: `{}`}},
	}

	err := client.CreateUser(context.Background(), WorksmobileUserPayload{Email: "tester@samaneng.com"})

	require.Error(t, err)
	require.Contains(t, err.Error(), "worksmobile directory token is not configured")
}

func TestWorksmobileHTTPClientRequestsJWTBearerAccessToken(t *testing.T) {
	privateKey := testRSAPrivateKeyPEM(t)
	transport := &captureRoundTripper{
		responses: []captureResponse{
			{statusCode: http.StatusOK, body: `{"access_token":"directory-token-from-jwt","token_type":"Bearer","expires_in":3600}`},
			{statusCode: http.StatusCreated, body: `{}`},
		},
	}
	client := &WorksmobileHTTPClient{
		BaseURL:    "https://works.example.test",
		HTTPClient: &http.Client{Transport: transport},
		now:        func() time.Time { return time.Unix(1710000000, 0) },
		OAuthConfig: WorksmobileOAuthConfig{
			ClientID:       "client-id-1",
			ClientSecret:   "client-secret-1",
			ServiceAccount: "service-account-1",
			PrivateKey:     privateKey,
			Scope:          "directory",
			TokenURL:       "https://auth.example.test/token",
		},
	}

	err := client.CreateUser(context.Background(), WorksmobileUserPayload{
		DomainID:        300285955,
		Email:           "tester@samaneng.com",
		UserExternalKey: "user-1",
		UserName:        WorksmobileUserName{LastName: "Tester"},
		PasswordConfig:  WorksmobilePasswordConfig{PasswordCreationType: "ADMIN", Password: "Aa1!Aa1!Aa1!Aa1!"},
	})

	require.NoError(t, err)
	require.Len(t, transport.requests, 2)
	require.Equal(t, "https://auth.example.test/token", transport.requests[0].URL.String())
	require.Equal(t, "/v1.0/users", transport.requests[1].URL.Path)
	require.Equal(t, "Bearer directory-token-from-jwt", transport.requests[1].Header.Get("Authorization"))

	form, err := url.ParseQuery(string(transport.requestBodies[0]))
	require.NoError(t, err)
	require.Equal(t, "urn:ietf:params:oauth:grant-type:jwt-bearer", form.Get("grant_type"))
	require.Equal(t, "client-id-1", form.Get("client_id"))
	require.Equal(t, "client-secret-1", form.Get("client_secret"))
	require.Equal(t, "directory", form.Get("scope"))

	parts := strings.Split(form.Get("assertion"), ".")
	require.Len(t, parts, 3)
	payloadData, err := base64.RawURLEncoding.DecodeString(parts[1])
	require.NoError(t, err)
	var payload map[string]any
	require.NoError(t, json.Unmarshal(payloadData, &payload))
	require.Equal(t, "client-id-1", payload["iss"])
	require.Equal(t, "service-account-1", payload["sub"])
	require.Equal(t, float64(1710000000), payload["iat"])
	require.Equal(t, float64(1710003600), payload["exp"])
}

func TestWorksmobileHTTPClientAppliesRateLimitBeforeDirectoryAPICalls(t *testing.T) {
	transport := &captureRoundTripper{
		statusCode: http.StatusCreated,
		body:       `{}`,
	}
	limiter := &captureWorksmobileRateLimiter{}
	client := &WorksmobileHTTPClient{
		BaseURL:        "https://works.example.test",
		DirectoryToken: "directory-token-1",
		HTTPClient:     &http.Client{Transport: transport},
		RateLimiter:    limiter,
	}

	err := client.CreateUser(context.Background(), WorksmobileUserPayload{
		Email:          "tester@samaneng.com",
		PasswordConfig: WorksmobilePasswordConfig{PasswordCreationType: "ADMIN", Password: "Aa1!Aa1!Aa1!Aa1!"},
	})

	require.NoError(t, err)
	require.Equal(t, []string{"POST /v1.0/users"}, limiter.keys)
	require.Len(t, transport.requests, 1)
}

func TestWorksmobileHTTPClientAppliesRateLimitBeforeOAuthTokenCalls(t *testing.T) {
	privateKey := testRSAPrivateKeyPEM(t)
	transport := &captureRoundTripper{
		statusCode: http.StatusOK,
		body:       `{"access_token":"directory-token-from-jwt","token_type":"Bearer","expires_in":3600}`,
	}
	limiter := &captureWorksmobileRateLimiter{}
	client := &WorksmobileHTTPClient{
		HTTPClient:  &http.Client{Transport: transport},
		RateLimiter: limiter,
		now:         func() time.Time { return time.Unix(1710000000, 0) },
		OAuthConfig: WorksmobileOAuthConfig{
			ClientID:       "client-id-1",
			ClientSecret:   "client-secret-1",
			ServiceAccount: "service-account-1",
			PrivateKey:     privateKey,
			Scope:          "directory",
			TokenURL:       "https://auth.example.test/oauth2/v2.0/token",
		},
	}

	_, _, err := client.requestDirectoryAccessToken(context.Background(), time.Unix(1710000000, 0))

	require.NoError(t, err)
	require.Equal(t, []string{"POST /oauth2/v2.0/token"}, limiter.keys)
	require.Len(t, transport.requests, 1)
}

func TestWorksmobileHTTPClientRateLimitKeyNormalizesResourceIDsAndDropsQuery(t *testing.T) {
	parsedURL, err := url.Parse("https://works.example.test/v1.0/users/user%40example.com/alias-emails/alias%40example.com?domainId=1")
	require.NoError(t, err)

	require.Equal(
		t,
		"POST /v1.0/users/{id}/alias-emails/{id}",
		worksmobileRateLimitKey(http.MethodPost, parsedURL),
	)

	parsedURL, err = url.Parse("https://works.example.test/scim/v2/Users/works-user-1")
	require.NoError(t, err)
	require.Equal(t, "PATCH /scim/v2/Users/{id}", worksmobileRateLimitKey(http.MethodPatch, parsedURL))
}

func TestNewWorksmobileHTTPClientDoesNotInstallRateLimiterByDefault(t *testing.T) {
	client := NewWorksmobileHTTPClientWithAuth("directory-token", "scim-token", WorksmobileOAuthConfig{})

	require.Nil(t, client.RateLimiter)
}

func TestNewWorksmobileAPIRateLimiterCreatesLimiterForWorkerUse(t *testing.T) {
	limiter := NewWorksmobileAPIRateLimiter(240, time.Minute)

	require.NotNil(t, limiter)
}

func TestWorksmobileHTTPClientRequiresConfiguredAPIBaseURL(t *testing.T) {
	client := &WorksmobileHTTPClient{
		DirectoryToken: "directory-token-1",
		HTTPClient:     &http.Client{Transport: &captureRoundTripper{statusCode: http.StatusCreated, body: `{}`}},
	}

	err := client.CreateUser(context.Background(), WorksmobileUserPayload{
		DomainID:        300285955,
		Email:           "tester@samaneng.com",
		UserExternalKey: "user-1",
		UserName:        WorksmobileUserName{LastName: "Tester"},
		PasswordConfig:  WorksmobilePasswordConfig{PasswordCreationType: "ADMIN", Password: "Aa1!Aa1!Aa1!Aa1!"},
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "worksmobile api base url is not configured")
}

func TestWorksmobileHTTPClientRequiresConfiguredOAuthTokenURL(t *testing.T) {
	privateKey := testRSAPrivateKeyPEM(t)
	client := &WorksmobileHTTPClient{
		BaseURL: "https://works.example.test",
		OAuthConfig: WorksmobileOAuthConfig{
			ClientID:       "client-id-1",
			ClientSecret:   "client-secret-1",
			ServiceAccount: "service-account-1",
			PrivateKey:     privateKey,
			Scope:          "directory",
		},
	}

	_, _, err := client.requestDirectoryAccessToken(
		context.Background(),
		time.Unix(1710000000, 0),
	)

	require.Error(t, err)
	require.Contains(t, err.Error(), "worksmobile oauth token url is not configured")
}

func TestWorksmobileHTTPClientListUsersUsesDirectoryAPIFirst(t *testing.T) {
	t.Setenv("SAMAN_DOMAIN_ID", "300285955")
	transport := &captureRoundTripper{
		responses: []captureResponse{
			{statusCode: http.StatusOK, body: `{"users":[{"userId":"works-user-1","userExternalKey":"user-1","email":"tester@samaneng.com","userName":{"lastName":"Tester"},"organizations":[{"primary":true,"orgUnits":[{"orgUnitId":"works-org-1","orgUnitName":"삼안"}]}]}],"responseMetaData":{}}`},
		},
	}
	client := &WorksmobileHTTPClient{
		BaseURL:        "https://works.example.test",
		DirectoryToken: "directory-token-1",
		SCIMToken:      "scim-token-1",
		DomainIDs:      []int64{300285955},
		HTTPClient:     &http.Client{Transport: transport},
	}

	users, err := client.ListUsers(context.Background())

	require.NoError(t, err)
	require.Len(t, users, 1)
	require.Equal(t, "user-1", users[0].ExternalID)
	require.Equal(t, "tester@samaneng.com", users[0].Email)
	require.Equal(t, int64(300285955), users[0].DomainID)
	require.Equal(t, "삼안", users[0].DomainName)
	require.Equal(t, "works-org-1", users[0].PrimaryOrgUnitID)
	require.Len(t, transport.requests, 1)
	require.Equal(t, "/v1.0/users", transport.requests[0].URL.Path)
	require.Equal(t, "300285955", transport.requests[0].URL.Query().Get("domainId"))
}

func TestWorksmobileHTTPClientListUsersFallsBackToSCIMWhenDirectoryFails(t *testing.T) {
	transport := &captureRoundTripper{
		responses: []captureResponse{
			{statusCode: http.StatusForbidden, body: `{"code":"FORBIDDEN"}`},
			{statusCode: http.StatusOK, body: `{"totalResults":1,"Resources":[{"id":"scim-user-1","userName":"tester@samaneng.com","displayName":"Tester","emails":[]}]} `},
		},
	}
	client := &WorksmobileHTTPClient{
		BaseURL:        "https://works.example.test",
		DirectoryToken: "directory-token-1",
		SCIMToken:      "scim-token-1",
		DomainIDs:      []int64{300285955},
		HTTPClient:     &http.Client{Transport: transport},
	}

	users, err := client.ListUsers(context.Background())

	require.NoError(t, err)
	require.Len(t, users, 1)
	require.Equal(t, "scim-user-1", users[0].ID)
	require.Equal(t, "tester@samaneng.com", users[0].Email)
	require.Equal(t, "/v1.0/users", transport.requests[0].URL.Path)
	require.Equal(t, "/scim/v2/Users", transport.requests[1].URL.Path)
}

func TestWorksmobileHTTPClientSetUserActivePatchesSCIMActiveFlag(t *testing.T) {
	transport := &captureRoundTripper{
		responses: []captureResponse{
			{statusCode: http.StatusOK, body: `{"totalResults":1,"Resources":[{"id":"scim-user-1","externalId":"user-1","userName":"tester@samaneng.com","active":true,"emails":[{"value":"tester@samaneng.com","primary":true}]}]}`},
			{statusCode: http.StatusOK, body: `{}`},
		},
	}
	client := &WorksmobileHTTPClient{
		BaseURL:    "https://works.example.test",
		SCIMToken:  "scim-token-1",
		HTTPClient: &http.Client{Transport: transport},
	}

	err := client.SetUserActive(context.Background(), "tester@samaneng.com", false)

	require.NoError(t, err)
	require.Len(t, transport.requests, 2)
	require.Equal(t, http.MethodGet, transport.requests[0].Method)
	require.Equal(t, "/scim/v2/Users", transport.requests[0].URL.Path)
	require.Equal(t, http.MethodPatch, transport.requests[1].Method)
	require.Equal(t, "/scim/v2/Users/scim-user-1", transport.requests[1].URL.Path)
	require.Equal(t, "Bearer scim-token-1", transport.requests[1].Header.Get("Authorization"))

	var patchPayload map[string]any
	require.Len(t, transport.requestBodies, 1)
	require.NoError(t, json.Unmarshal(transport.requestBodies[0], &patchPayload))
	operations, ok := patchPayload["Operations"].([]any)
	require.True(t, ok)
	require.Len(t, operations, 1)
	operation, ok := operations[0].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "replace", operation["op"])
	require.Equal(t, "active", operation["path"])
	require.Equal(t, false, operation["value"])
}

func TestWorksmobileHTTPClientListGroupsUsesDirectoryAPIFirst(t *testing.T) {
	t.Setenv("SAMAN_DOMAIN_ID", "300285955")
	transport := &captureRoundTripper{
		responses: []captureResponse{
			{statusCode: http.StatusOK, body: `{"orgUnits":[{"orgUnitId":"works-org-1","orgUnitExternalKey":"tenant-1","orgUnitName":"삼안","parentOrgUnitId":"parent-1","parentOrgUnitName":"상위"}],"responseMetaData":{}}`},
		},
	}
	client := &WorksmobileHTTPClient{
		BaseURL:        "https://works.example.test",
		DirectoryToken: "directory-token-1",
		SCIMToken:      "scim-token-1",
		DomainIDs:      []int64{300285955},
		HTTPClient:     &http.Client{Transport: transport},
	}

	groups, err := client.ListGroups(context.Background())

	require.NoError(t, err)
	require.Len(t, groups, 1)
	require.Equal(t, "tenant-1", groups[0].ExternalID)
	require.Equal(t, "삼안", groups[0].DisplayName)
	require.Equal(t, int64(300285955), groups[0].DomainID)
	require.Equal(t, "삼안", groups[0].DomainName)
	require.Equal(t, "parent-1", groups[0].ParentID)
	require.Equal(t, "/v1.0/orgunits", transport.requests[0].URL.Path)
}

func TestWorksmobileHTTPClientUpsertOrgUnitBackfillsExternalKeyByMailLocalPart(t *testing.T) {
	transport := &captureRoundTripper{
		responses: []captureResponse{
			{statusCode: http.StatusConflict, body: `{"code":"CONFLICT"}`},
			{statusCode: http.StatusOK, body: `{"orgUnits":[{"orgUnitId":"works-org-1","orgUnitName":"기술개발센터","email":"tech-dev-center@samaneng.com"}],"responseMetaData":{}}`},
			{statusCode: http.StatusOK, body: `{}`},
		},
	}
	client := &WorksmobileHTTPClient{
		BaseURL:           "https://works.example.test",
		DirectoryToken:    "directory-token-1",
		DomainIDs:         []int64{300285955},
		HTTPClient:        &http.Client{Transport: transport},
		OrgUnitWriteDelay: -1,
	}

	err := client.UpsertOrgUnit(context.Background(), WorksmobileOrgUnitPayload{
		DomainID:           300285955,
		OrgUnitName:        "기술개발센터",
		OrgUnitExternalKey: "tenant-tech-dev-center",
		DisplayOrder:       0,
	}, "tech-dev-center")

	require.NoError(t, err)
	require.Len(t, transport.requests, 3)
	require.Equal(t, http.MethodPost, transport.requests[0].Method)
	require.Equal(t, "/v1.0/orgunits", transport.requests[0].URL.Path)
	require.Contains(t, string(transport.requestBodies[0]), `"displayOrder":1`)
	require.Equal(t, http.MethodGet, transport.requests[1].Method)
	require.Equal(t, "/v1.0/orgunits", transport.requests[1].URL.Path)
	require.Equal(t, http.MethodPatch, transport.requests[2].Method)
	require.Equal(t, "/v1.0/orgunits/works-org-1", transport.requests[2].URL.Path)
	require.Contains(t, string(transport.requestBodies[1]), `"orgUnitExternalKey":"tenant-tech-dev-center"`)
}

func TestWorksmobileHTTPClientUpsertOrgUnitDoesNotBackfillExternalKeyByName(t *testing.T) {
	transport := &captureRoundTripper{
		responses: []captureResponse{
			{statusCode: http.StatusConflict, body: `{"code":"CONFLICT"}`},
			{statusCode: http.StatusOK, body: `{"orgUnits":[{"orgUnitId":"works-org-1","orgUnitName":"기술개발센터","email":"legacy-tech@samaneng.com"}],"responseMetaData":{}}`},
		},
	}
	client := &WorksmobileHTTPClient{
		BaseURL:           "https://works.example.test",
		DirectoryToken:    "directory-token-1",
		DomainIDs:         []int64{300285955},
		HTTPClient:        &http.Client{Transport: transport},
		OrgUnitWriteDelay: -1,
	}

	err := client.UpsertOrgUnit(context.Background(), WorksmobileOrgUnitPayload{
		DomainID:           300285955,
		OrgUnitName:        "기술개발센터",
		OrgUnitExternalKey: "tenant-tech-dev-center",
		DisplayOrder:       1,
	}, "tech-dev-center")

	require.Error(t, err)
	require.Contains(t, err.Error(), "external key match not found")
	require.Len(t, transport.requests, 2)
	require.Equal(t, http.MethodGet, transport.requests[1].Method)
}

func TestWorksmobileHTTPClientUpsertOrgUnitTreatsExistingExternalKeyConflictAsSuccess(t *testing.T) {
	transport := &captureRoundTripper{
		responses: []captureResponse{
			{statusCode: http.StatusConflict, body: `{"code":"CONFLICT"}`},
			{statusCode: http.StatusOK, body: `{"orgUnits":[{"orgUnitId":"works-org-1","orgUnitExternalKey":"tenant-tech-dev-center","orgUnitName":"기술개발센터"}],"responseMetaData":{}}`},
		},
	}
	client := &WorksmobileHTTPClient{
		BaseURL:        "https://works.example.test",
		DirectoryToken: "directory-token-1",
		DomainIDs:      []int64{300285955},
		HTTPClient:     &http.Client{Transport: transport},
	}

	err := client.UpsertOrgUnit(context.Background(), WorksmobileOrgUnitPayload{
		DomainID:           300285955,
		OrgUnitName:        "기술개발센터",
		OrgUnitExternalKey: "tenant-tech-dev-center",
	}, "")

	require.NoError(t, err)
	require.Len(t, transport.requests, 3)
	require.Equal(t, http.MethodPost, transport.requests[0].Method)
	require.Equal(t, http.MethodGet, transport.requests[1].Method)
	require.Equal(t, http.MethodPatch, transport.requests[2].Method)
	require.Contains(t, string(transport.requestBodies[1]), `"orgUnitExternalKey":"tenant-tech-dev-center"`)
}

func TestWorksmobileLiveJWTTokenExchange(t *testing.T) {
	if os.Getenv("WORKSMOBILE_LIVE_JWT_TOKEN_EXCHANGE") != "1" {
		t.Skip("live Worksmobile token exchange is disabled")
	}
	client := newWorksmobileLiveClient()

	token, err := client.directoryAccessToken(context.Background())

	require.NoError(t, err)
	require.NotEmpty(t, token)
}

func TestWorksmobileRelayWorkerProcessesUserCreateAndMarksProcessed(t *testing.T) {
	repo := &fakeWorksmobileOutboxRepo{
		ready: []domain.WorksmobileOutbox{
			{
				ID:           "job-1",
				ResourceType: domain.WorksmobileResourceUser,
				ResourceID:   "user-1",
				Action:       domain.WorksmobileActionUpsert,
				Status:       domain.WorksmobileOutboxStatusPending,
				Payload: worksmobileUserOutboxPayload("root-1", WorksmobileUserPayload{
					Email:           "tester@samaneng.com",
					UserExternalKey: "user-1",
					PasswordConfig: WorksmobilePasswordConfig{
						PasswordCreationType: "ADMIN",
						Password:             "Aa1!Aa1!Aa1!Aa1!",
					},
				}),
			},
		},
	}
	client := &fakeWorksmobileDirectoryClient{}
	worker := NewWorksmobileRelayWorker(repo, client)

	err := worker.ProcessOnce(context.Background())

	require.NoError(t, err)
	require.Equal(t, []string{"job-1"}, repo.processingIDs)
	require.Equal(t, []string{"job-1"}, repo.processedIDs)
	require.Equal(t, "tester@samaneng.com", client.createdUsers[0].Email)
}

func TestWorksmobileRelayWorkerRegistersAliasEmailsAfterUserUpsert(t *testing.T) {
	repo := &fakeWorksmobileOutboxRepo{
		ready: []domain.WorksmobileOutbox{
			{
				ID:           "job-1",
				ResourceType: domain.WorksmobileResourceUser,
				ResourceID:   "user-1",
				Action:       domain.WorksmobileActionUpsert,
				Status:       domain.WorksmobileOutboxStatusPending,
				Payload: worksmobileUserOutboxPayload("root-1", WorksmobileUserPayload{
					Email:           "ypshim@samaneng.com",
					UserExternalKey: "user-1",
					AliasEmails:     []string{"ypshim@hanmaceng.co.kr"},
					PasswordConfig: WorksmobilePasswordConfig{
						PasswordCreationType: "ADMIN",
						Password:             "Aa1!Aa1!Aa1!Aa1!",
					},
				}),
			},
		},
	}
	client := &fakeWorksmobileDirectoryClient{}
	worker := NewWorksmobileRelayWorker(repo, client)

	err := worker.ProcessOnce(context.Background())

	require.NoError(t, err)
	require.Equal(t, []string{"job-1"}, repo.processedIDs)
	require.Equal(t, "ypshim@samaneng.com", client.createdUsers[0].Email)
	require.Empty(t, client.createdUsers[0].AliasEmails)
	require.Equal(t, []string{"ypshim@samaneng.com:ypshim@hanmaceng.co.kr"}, client.aliasEmails)
}

func TestWorksmobileRelayWorkerProcessesUserPasswordResetAndMarksProcessed(t *testing.T) {
	repo := &fakeWorksmobileOutboxRepo{
		ready: []domain.WorksmobileOutbox{
			{
				ID:           "job-reset",
				ResourceType: domain.WorksmobileResourceUser,
				ResourceID:   "user-1",
				Action:       domain.WorksmobileActionPasswordReset,
				Status:       domain.WorksmobileOutboxStatusPending,
				Payload: domain.JSONMap{
					"loginEmail": "target@samaneng.com",
					"request": map[string]any{
						"email": "target@samaneng.com",
						"passwordConfig": map[string]any{
							"passwordCreationType": "ADMIN",
							"password":             "Aa1!Aa1!Aa1!Aa1!",
						},
					},
				},
			},
		},
	}
	client := &fakeWorksmobileDirectoryClient{}
	worker := NewWorksmobileRelayWorker(repo, client)

	err := worker.ProcessOnce(context.Background())

	require.NoError(t, err)
	require.Equal(t, []string{"job-reset"}, repo.processedIDs)
	require.Equal(t, []string{"target@samaneng.com:Aa1!Aa1!Aa1!Aa1!"}, client.passwordResets)
}

func TestWorksmobileRelayWorkerProcessesUserSuspendAndMarksProcessed(t *testing.T) {
	repo := &fakeWorksmobileOutboxRepo{
		ready: []domain.WorksmobileOutbox{
			{
				ID:           "job-1",
				ResourceType: domain.WorksmobileResourceUser,
				ResourceID:   "user-1",
				Action:       domain.WorksmobileActionSuspend,
				Status:       domain.WorksmobileOutboxStatusPending,
				Payload: worksmobileUserOutboxPayload("root-1", WorksmobileUserPayload{
					Email:           "tester@samaneng.com",
					UserExternalKey: "user-1",
				}),
			},
		},
	}
	client := &fakeWorksmobileDirectoryClient{}
	worker := NewWorksmobileRelayWorker(repo, client)

	err := worker.ProcessOnce(context.Background())

	require.NoError(t, err)
	require.Equal(t, []string{"job-1"}, repo.processingIDs)
	require.Equal(t, []string{"job-1"}, repo.processedIDs)
	require.Equal(t, []string{"tester@samaneng.com"}, client.suspendedUsers)
}

func TestWorksmobileRelayWorkerProcessOnceRecoversPanic(t *testing.T) {
	repo := &fakeWorksmobileOutboxRepo{listReadyPanic: "list ready crashed"}
	client := &fakeWorksmobileDirectoryClient{}
	worker := NewWorksmobileRelayWorker(repo, client)

	err := worker.ProcessOnce(context.Background())

	require.Error(t, err)
	require.Contains(t, err.Error(), "worksmobile relay panic")
	require.Contains(t, err.Error(), "list ready crashed")
}

func TestWorksmobileRelayWorkerProcessesActiveUserUpsertAndReactivates(t *testing.T) {
	repo := &fakeWorksmobileOutboxRepo{
		ready: []domain.WorksmobileOutbox{
			{
				ID:           "job-1",
				ResourceType: domain.WorksmobileResourceUser,
				ResourceID:   "user-1",
				Action:       domain.WorksmobileActionUpsert,
				Status:       domain.WorksmobileOutboxStatusPending,
				Payload: worksmobileUserOutboxPayload("root-1", WorksmobileUserPayload{
					Email:           "tester@samaneng.com",
					UserExternalKey: "user-1",
				}, domain.UserStatusActive),
			},
		},
	}
	client := &fakeWorksmobileDirectoryClient{}
	worker := NewWorksmobileRelayWorker(repo, client)

	err := worker.ProcessOnce(context.Background())

	require.NoError(t, err)
	require.Equal(t, []string{"job-1"}, repo.processedIDs)
	require.Equal(t, "tester@samaneng.com", client.createdUsers[0].Email)
	require.Equal(t, []string{"tester@samaneng.com"}, client.activeUsers)
}

func TestWorksmobileRelayWorkerProcessesOrgUnitDeleteAndMarksProcessed(t *testing.T) {
	repo := &fakeWorksmobileOutboxRepo{
		ready: []domain.WorksmobileOutbox{
			{
				ID:           "job-1",
				ResourceType: domain.WorksmobileResourceOrgUnit,
				ResourceID:   "works-org-1",
				Action:       domain.WorksmobileActionDelete,
				Status:       domain.WorksmobileOutboxStatusPending,
				Payload:      domain.JSONMap{"worksmobileId": "works-org-1"},
			},
		},
	}
	client := &fakeWorksmobileDirectoryClient{}
	worker := NewWorksmobileRelayWorker(repo, client)

	err := worker.ProcessOnce(context.Background())

	require.NoError(t, err)
	require.Equal(t, []string{"job-1"}, repo.processedIDs)
	require.Equal(t, []string{"works-org-1"}, client.deletedOrgUnits)
}

func TestWorksmobileRelayWorkerProcessesOrgUnitParentsBeforeChildren(t *testing.T) {
	repo := &fakeWorksmobileOutboxRepo{
		ready: []domain.WorksmobileOutbox{
			{
				ID:           "job-child",
				ResourceType: domain.WorksmobileResourceOrgUnit,
				ResourceID:   "child-tenant",
				Action:       domain.WorksmobileActionUpsert,
				Status:       domain.WorksmobileOutboxStatusPending,
				Payload: domain.JSONMap{
					"request": map[string]any{
						"domainId":           300293726,
						"orgUnitExternalKey": "child-tenant",
						"orgUnitName":        "child",
						"parentOrgUnitId":    "externalKey:parent-tenant",
					},
				},
			},
			{
				ID:           "job-parent",
				ResourceType: domain.WorksmobileResourceOrgUnit,
				ResourceID:   "parent-tenant",
				Action:       domain.WorksmobileActionUpsert,
				Status:       domain.WorksmobileOutboxStatusPending,
				Payload: domain.JSONMap{
					"request": map[string]any{
						"domainId":           300293726,
						"orgUnitExternalKey": "parent-tenant",
						"orgUnitName":        "parent",
					},
				},
			},
		},
	}
	client := &fakeWorksmobileDirectoryClient{}
	worker := NewWorksmobileRelayWorker(repo, client)

	err := worker.ProcessOnce(context.Background())

	require.NoError(t, err)
	require.Equal(t, []string{"job-parent", "job-child"}, repo.processingIDs)
	require.Equal(t, []string{"parent-tenant", "child-tenant"}, []string{
		client.createdOrgUnits[0].OrgUnitExternalKey,
		client.createdOrgUnits[1].OrgUnitExternalKey,
	})
}

func TestWorksmobileRelayWorkerSkipsDispatchWhenJobClaimFails(t *testing.T) {
	repo := &fakeWorksmobileOutboxRepo{
		markProcessingClaims: map[string]bool{"job-claimed-by-other-worker": false},
		ready: []domain.WorksmobileOutbox{
			{
				ID:           "job-claimed-by-other-worker",
				ResourceType: domain.WorksmobileResourceOrgUnit,
				ResourceID:   "org-1",
				Action:       domain.WorksmobileActionUpsert,
				Status:       domain.WorksmobileOutboxStatusPending,
				Payload: domain.JSONMap{
					"request": map[string]any{
						"domainId":           300293726,
						"orgUnitExternalKey": "org-1",
						"orgUnitName":        "org",
					},
				},
			},
		},
	}
	client := &fakeWorksmobileDirectoryClient{}
	worker := NewWorksmobileRelayWorker(repo, client)

	err := worker.ProcessOnce(context.Background())

	require.NoError(t, err)
	require.Empty(t, repo.processingIDs)
	require.Empty(t, repo.processedIDs)
	require.Empty(t, client.createdOrgUnits)
}

func TestWorksmobileRelayWorkerSkipsProcessingWhenLeaderLockIsNotHeld(t *testing.T) {
	repo := &fakeWorksmobileOutboxRepo{
		ready: []domain.WorksmobileOutbox{
			{
				ID:           "job-1",
				ResourceType: domain.WorksmobileResourceUser,
				ResourceID:   "user-1",
				Action:       domain.WorksmobileActionUpsert,
				Status:       domain.WorksmobileOutboxStatusPending,
				Payload: worksmobileUserOutboxPayload("root-1", WorksmobileUserPayload{
					Email:           "tester@samaneng.com",
					UserExternalKey: "user-1",
				}),
			},
		},
	}
	client := &fakeWorksmobileDirectoryClient{}
	worker := NewWorksmobileRelayWorker(repo, client)
	worker.SetLeaderLock(&fakeWorksmobileRelayLeaderLock{held: false})

	err := worker.ProcessOnce(context.Background())

	require.NoError(t, err)
	require.Zero(t, repo.listReadyCalls)
	require.Empty(t, repo.processingIDs)
	require.Empty(t, repo.processedIDs)
	require.Empty(t, client.createdUsers)
}

func TestWorksmobileRelayWorkerProcessesWhenLeaderLockIsHeld(t *testing.T) {
	repo := &fakeWorksmobileOutboxRepo{
		ready: []domain.WorksmobileOutbox{
			{
				ID:           "job-1",
				ResourceType: domain.WorksmobileResourceUser,
				ResourceID:   "user-1",
				Action:       domain.WorksmobileActionUpsert,
				Status:       domain.WorksmobileOutboxStatusPending,
				Payload: worksmobileUserOutboxPayload("root-1", WorksmobileUserPayload{
					Email:           "tester@samaneng.com",
					UserExternalKey: "user-1",
				}),
			},
		},
	}
	client := &fakeWorksmobileDirectoryClient{}
	lock := &fakeWorksmobileRelayLeaderLock{held: true}
	worker := NewWorksmobileRelayWorker(repo, client)
	worker.SetLeaderLock(lock)

	err := worker.ProcessOnce(context.Background())

	require.NoError(t, err)
	require.Equal(t, 1, lock.ensureCalls)
	require.Equal(t, 1, repo.listReadyCalls)
	require.Equal(t, []string{"job-1"}, repo.processedIDs)
	require.Equal(t, "tester@samaneng.com", client.createdUsers[0].Email)
}

func TestRedactWorksmobileOutboxPayloadsRemovesInitialPasswordFromOverview(t *testing.T) {
	jobs := []domain.WorksmobileOutbox{
		{
			ID: "job-1",
			Payload: domain.JSONMap{
				"loginEmail":      "tester@samaneng.com",
				"initialPassword": "Aa1!Aa1!Aa1!Aa1!",
			},
		},
	}

	redacted := redactWorksmobileOutboxPayloads(jobs)

	require.Equal(t, "tester@samaneng.com", redacted[0].Payload["loginEmail"])
	require.NotContains(t, redacted[0].Payload, "initialPassword")
}

func TestCompareWorksmobileUsersHidesMatchedByDefault(t *testing.T) {
	localUsers := []domain.User{
		{ID: "user-1", Email: "matched@samaneng.com", Name: "Matched"},
		{ID: "user-2", Email: "missing@samaneng.com", Name: "Missing"},
	}
	remoteUsers := []WorksmobileRemoteUser{
		{ID: "works-1", ExternalID: "user-1", Email: "matched@samaneng.com", DisplayName: "Matched"},
	}

	diffOnly := compareWorksmobileUsers(localUsers, remoteUsers, false, nil)
	all := compareWorksmobileUsers(localUsers, remoteUsers, true, nil)

	require.Len(t, diffOnly, 1)
	require.Equal(t, "missing_in_worksmobile", diffOnly[0].Status)
	require.Len(t, all, 2)
	require.Equal(t, "matched", all[0].Status)
}

func TestCompareWorksmobileUsersIncludesBaronAndWorksPrimaryOrg(t *testing.T) {
	tenantID := "tenant-primary"
	localUsers := []domain.User{
		{ID: "user-1", Email: "matched@samaneng.com", Name: "Matched", TenantID: &tenantID},
	}
	localTenants := map[string]domain.Tenant{
		tenantID: {ID: tenantID, Name: "기술기획", Slug: "tech-planning"},
	}
	remoteUsers := []WorksmobileRemoteUser{
		{
			ID:                 "works-1",
			ExternalID:         "user-1",
			Email:              "matched@samaneng.com",
			DisplayName:        "Matched",
			DomainID:           300285955,
			DomainName:         "삼안",
			PrimaryOrgUnitID:   "works-org-1",
			PrimaryOrgUnitName: "WORKS 기술기획",
		},
	}

	items := compareWorksmobileUsers(localUsers, remoteUsers, true, localTenants)

	require.Len(t, items, 1)
	require.Equal(t, tenantID, items[0].BaronPrimaryOrgID)
	require.Equal(t, "기술기획", items[0].BaronPrimaryOrgName)
	require.Equal(t, int64(300285955), items[0].WorksmobileDomainID)
	require.Equal(t, "삼안", items[0].WorksmobileDomainName)
	require.Equal(t, "works-org-1", items[0].WorksmobilePrimaryOrgID)
	require.Equal(t, "WORKS 기술기획", items[0].WorksmobilePrimaryOrgName)
}

func TestCompareWorksmobileUsersMarksEmailMatchWithoutExternalIDNeedsUpdate(t *testing.T) {
	localUsers := []domain.User{
		{ID: "user-1", Email: "tester@samaneng.com", Name: "Tester"},
	}
	remoteUsers := []WorksmobileRemoteUser{
		{ID: "works-1", ExternalID: "", Email: "tester@samaneng.com", DisplayName: "Tester"},
	}

	diffOnly := compareWorksmobileUsers(localUsers, remoteUsers, false, nil)
	all := compareWorksmobileUsers(localUsers, remoteUsers, true, nil)

	require.Len(t, diffOnly, 1)
	require.Equal(t, "needs_update", diffOnly[0].Status)
	require.Len(t, all, 1)
	require.Equal(t, "needs_update", all[0].Status)
	require.Equal(t, "works-1", all[0].WorksmobileID)
	require.Empty(t, all[0].ExternalKey)
}

func TestCompareWorksmobileUsersIncludesRecentFailedJobForMissingUser(t *testing.T) {
	localUsers := []domain.User{
		{ID: "user-1", Email: "missing@samaneng.com", Name: "Missing"},
	}
	jobSummaries := map[string]worksmobileUserJobSummary{
		"user-1": {
			Status:        domain.WorksmobileOutboxStatusFailed,
			RetryCount:    3,
			LastError:     "worksmobile api failed",
			LastAttemptAt: "2026-06-01T05:00:00Z",
		},
	}

	items := compareWorksmobileUsers(localUsers, nil, false, nil, jobSummaries)

	require.Len(t, items, 1)
	require.Equal(t, "missing_in_worksmobile", items[0].Status)
	require.Equal(t, domain.WorksmobileOutboxStatusFailed, items[0].WorksmobileJobStatus)
	require.Equal(t, 3, items[0].WorksmobileJobRetryCount)
	require.Equal(t, "worksmobile api failed", items[0].WorksmobileLastError)
	require.Equal(t, "2026-06-01T05:00:00Z", items[0].WorksmobileLastAttemptAt)
}

func TestCompareWorksmobileUsersIncludesWorksOnlyRowsWithoutExternalIDWhenIncludingMatched(t *testing.T) {
	remoteUsers := []WorksmobileRemoteUser{
		{ID: "works-1", ExternalID: "", Email: "works-only@samaneng.com", DisplayName: "Works Only"},
	}

	items := compareWorksmobileUsers(nil, remoteUsers, true, nil)

	require.Len(t, items, 1)
	require.Equal(t, "missing_external_key", items[0].Status)
	require.Equal(t, "works-1", items[0].WorksmobileID)
	require.Equal(t, "works-only@samaneng.com", items[0].WorksmobileEmail)
}

func TestCompareWorksmobileGroupsIncludesBaronAndWorksParentOrg(t *testing.T) {
	parentID := "tenant-parent"
	childID := "tenant-child"
	localTenants := []domain.Tenant{
		{ID: parentID, Slug: "tech-hq", Name: "기술본부", Type: domain.TenantTypeOrganization},
		{ID: childID, Slug: "tech-planning", Name: "기술기획", Type: domain.TenantTypeOrganization, ParentID: &parentID},
	}
	remoteGroups := []WorksmobileRemoteGroup{
		{
			ID:          "works-parent",
			ExternalID:  parentID,
			DisplayName: "WORKS 기술본부",
			DomainID:    300286337,
			DomainName:  "총괄기획&기술개발센터",
		},
		{
			ID:          "works-child",
			ExternalID:  childID,
			DisplayName: "WORKS 기술기획",
			DomainID:    300286337,
			DomainName:  "총괄기획&기술개발센터",
			ParentID:    "works-parent",
			ParentName:  "WORKS 기술본부",
		},
	}

	items := compareWorksmobileGroups(localTenants, remoteGroups, true)

	require.Len(t, items, 2)
	require.Equal(t, "tech-planning", items[1].BaronSlug)
	require.Equal(t, parentID, items[1].BaronParentID)
	require.Equal(t, "tech-hq", items[1].BaronParentSlug)
	require.Equal(t, "기술본부", items[1].BaronParentName)
	require.Equal(t, int64(300286337), items[1].WorksmobileDomainID)
	require.Equal(t, "총괄기획&기술개발센터", items[1].WorksmobileDomainName)
	require.Equal(t, "works-parent", items[1].WorksmobileParentID)
	require.Equal(t, "WORKS 기술본부", items[1].WorksmobileParentName)
}

func TestCompareWorksmobileGroupsDoesNotMatchDomainCompanyByDomainID(t *testing.T) {
	t.Setenv("SAMAN_DOMAIN_ID", "1001")
	parentID := "root-tenant"
	localTenants := []domain.Tenant{
		{
			ID:       "company-saman",
			Name:     "삼안",
			Type:     domain.TenantTypeCompany,
			ParentID: &parentID,
			Domains:  []domain.TenantDomain{{Domain: "samaneng.com"}},
		},
	}
	remoteGroups := []WorksmobileRemoteGroup{
		{
			ID:          "works-org-1",
			DisplayName: "WORKS 전용 조직",
			DomainID:    1001,
			DomainName:  "삼안",
		},
	}

	items := compareWorksmobileGroups(localTenants, remoteGroups, true)

	require.Len(t, items, 1)
	require.Empty(t, items[0].BaronID)
	require.Equal(t, "missing_external_key", items[0].Status)
}

func TestCompareWorksmobileGroupsIncludesWorksOnlyRowsWithoutExternalIDWhenIncludingMatched(t *testing.T) {
	remoteGroups := []WorksmobileRemoteGroup{
		{ID: "works-group-1", ExternalID: "", DisplayName: "WORKS 전용 조직"},
	}

	items := compareWorksmobileGroups(nil, remoteGroups, true)

	require.Len(t, items, 1)
	require.Equal(t, "missing_external_key", items[0].Status)
	require.Equal(t, "works-group-1", items[0].WorksmobileID)
	require.Equal(t, "WORKS 전용 조직", items[0].WorksmobileName)
}

func TestCompareWorksmobileGroupsDoesNotMatchBySlugLocalPartWhenExternalIDMissing(t *testing.T) {
	localTenants := []domain.Tenant{
		{
			ID:   "tenant-tech-dev-center",
			Slug: "tech-dev-center",
			Name: "기술개발센터",
			Type: domain.TenantTypeOrganization,
		},
	}
	remoteGroups := []WorksmobileRemoteGroup{
		{
			ID:            "works-org-1",
			DisplayName:   "기술개발센터",
			Email:         "tech-dev-center@samaneng.com",
			MailLocalPart: "tech-dev-center",
		},
	}

	diffOnly := compareWorksmobileGroups(localTenants, remoteGroups, false)
	all := compareWorksmobileGroups(localTenants, remoteGroups, true)

	require.Len(t, diffOnly, 2)
	require.Equal(t, "missing_in_worksmobile", diffOnly[0].Status)
	require.Equal(t, "tenant-tech-dev-center", diffOnly[0].BaronID)
	require.Equal(t, "missing_external_key", diffOnly[1].Status)
	require.Equal(t, "works-org-1", diffOnly[1].WorksmobileID)
	require.Len(t, all, 2)
	require.Equal(t, "missing_in_worksmobile", all[0].Status)
	require.Equal(t, "tenant-tech-dev-center", all[0].BaronID)
	require.Equal(t, "missing_external_key", all[1].Status)
	require.Equal(t, "works-org-1", all[1].WorksmobileID)
	require.Empty(t, all[1].ExternalKey)
}

func TestCompareWorksmobileGroupsDoesNotMatchByNameWhenExternalIDAndSlugAreMissing(t *testing.T) {
	localTenants := []domain.Tenant{
		{
			ID:   "tenant-tech-dev-center",
			Slug: "tech-dev-center",
			Name: "기술개발센터",
			Type: domain.TenantTypeOrganization,
		},
	}
	remoteGroups := []WorksmobileRemoteGroup{
		{
			ID:          "works-org-1",
			DisplayName: "기술개발센터",
		},
	}

	items := compareWorksmobileGroups(localTenants, remoteGroups, false)

	require.Len(t, items, 2)
	require.Equal(t, "missing_in_worksmobile", items[0].Status)
	require.Equal(t, "tenant-tech-dev-center", items[0].BaronID)
	require.Equal(t, "missing_external_key", items[1].Status)
	require.Equal(t, "works-org-1", items[1].WorksmobileID)
}

func TestCompareWorksmobileGroupsListsExternalKeyMissingRowsAsDeleteCandidatesAcrossDomains(t *testing.T) {
	t.Setenv("SAMAN_DOMAIN_ID", "1001")
	t.Setenv("HANMAC_DOMAIN_ID", "1002")
	t.Setenv("GPDTDC_DOMAIN_ID", "1003")
	t.Setenv("BARONGROUP_DOMAIN_ID", "1004")
	rootID := "root-tenant"
	samanID := "company-saman"
	hanmacID := "company-hanmac"
	localTenants := []domain.Tenant{
		{ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
		{ID: samanID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
		{ID: hanmacID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "hanmaceng.co.kr"}}},
		{ID: "tenant-saman-planning", Slug: "planning", Name: "기획팀", Type: domain.TenantTypeOrganization, ParentID: &samanID},
		{ID: "tenant-hanmac-planning", Slug: "planning", Name: "기획팀", Type: domain.TenantTypeOrganization, ParentID: &hanmacID},
	}
	remoteGroups := []WorksmobileRemoteGroup{
		{ID: "works-saman-planning", DomainID: 1001, DisplayName: "기획팀", MailLocalPart: "planning"},
		{ID: "works-hanmac-planning", DomainID: 1002, DisplayName: "기획팀", MailLocalPart: "planning"},
	}

	items := compareWorksmobileGroups(localTenants, remoteGroups, false)

	require.Len(t, items, 4)
	require.Equal(t, "tenant-saman-planning", items[0].BaronID)
	require.Equal(t, "missing_in_worksmobile", items[0].Status)
	require.Equal(t, "tenant-hanmac-planning", items[1].BaronID)
	require.Equal(t, "missing_in_worksmobile", items[1].Status)
	require.Equal(t, "works-saman-planning", items[2].WorksmobileID)
	require.Equal(t, "missing_external_key", items[2].Status)
	require.Equal(t, "works-hanmac-planning", items[3].WorksmobileID)
	require.Equal(t, "missing_external_key", items[3].Status)
}

func TestParseWorksmobileRemoteUserUsesUserNameEmailWhenEmailsAreEmpty(t *testing.T) {
	user := parseWorksmobileRemoteUser(map[string]any{
		"id":          "works-1",
		"userName":    "tester@samaneng.com",
		"displayName": "Tester",
		"emails":      []any{},
	})

	require.Equal(t, "tester@samaneng.com", user.UserName)
	require.Equal(t, "tester@samaneng.com", user.Email)
}

func TestParseWorksmobileRemoteResourcesExtractsOrgFields(t *testing.T) {
	user := parseWorksmobileRemoteUser(map[string]any{
		"id":         "works-user",
		"externalId": "user-1",
		"organizations": []any{
			map[string]any{
				"primary":     true,
				"orgUnitId":   "works-org-1",
				"orgUnitName": "WORKS 기술기획",
			},
		},
	})
	group := parseWorksmobileRemoteGroup(map[string]any{
		"id":         "works-group",
		"externalId": "group-1",
		"parent": map[string]any{
			"id":          "works-parent",
			"displayName": "WORKS 기술본부",
		},
	})

	require.Equal(t, "works-org-1", user.PrimaryOrgUnitID)
	require.Equal(t, "WORKS 기술기획", user.PrimaryOrgUnitName)
	require.Equal(t, "works-parent", group.ParentID)
	require.Equal(t, "WORKS 기술본부", group.ParentName)
}

func TestParseWorksmobileDirectoryUserIncludesFullNameLevelAndOrgRole(t *testing.T) {
	user := parseWorksmobileDirectoryUser(map[string]any{
		"userId": "works-user",
		"email":  "tester@samaneng.com",
		"userName": map[string]any{
			"lastName":  "홍",
			"firstName": "길동",
		},
		"levelId":   "level-1",
		"levelName": "책임",
		"task":      "기술검토",
		"organizations": []any{
			map[string]any{
				"primary": true,
				"orgUnits": []any{
					map[string]any{
						"orgUnitId":    "works-org-1",
						"orgUnitName":  "기술기획",
						"positionId":   "position-1",
						"positionName": "팀장",
						"isManager":    true,
					},
				},
			},
		},
	})

	require.Equal(t, "홍길동", user.DisplayName)
	require.Equal(t, "level-1", user.LevelID)
	require.Equal(t, "책임", user.LevelName)
	require.Equal(t, "기술검토", user.Task)
	require.Equal(t, "works-org-1", user.PrimaryOrgUnitID)
	require.Equal(t, "기술기획", user.PrimaryOrgUnitName)
	require.Equal(t, "position-1", user.PrimaryOrgUnitPositionID)
	require.Equal(t, "팀장", user.PrimaryOrgUnitPositionName)
	require.NotNil(t, user.PrimaryOrgUnitIsManager)
	require.True(t, *user.PrimaryOrgUnitIsManager)
	require.NotNil(t, user.OrgUnitManagers["works-org-1"])
	require.True(t, *user.OrgUnitManagers["works-org-1"])
}

func TestParseWorksmobileDirectoryUserIncludesAllOrgUnitManagerFlags(t *testing.T) {
	user := parseWorksmobileDirectoryUser(map[string]any{
		"userId":         "works-user",
		"email":          "tester@samaneng.com",
		"cellPhone":      "010-1234-5678",
		"employeeNumber": "EMP001",
		"userName": map[string]any{
			"lastName": "홍길동",
		},
		"organizations": []any{
			map[string]any{
				"primary": true,
				"orgUnits": []any{
					map[string]any{
						"orgUnitId": "externalKey:primary-org",
						"primary":   true,
						"isManager": false,
					},
					map[string]any{
						"orgUnitId": "externalKey:secondary-org",
						"primary":   false,
						"isManager": true,
					},
				},
			},
		},
	})

	require.Len(t, user.OrgUnitManagers, 2)
	require.NotNil(t, user.OrgUnitManagers["externalKey:primary-org"])
	require.False(t, *user.OrgUnitManagers["externalKey:primary-org"])
	require.NotNil(t, user.OrgUnitManagers["externalKey:secondary-org"])
	require.True(t, *user.OrgUnitManagers["externalKey:secondary-org"])
	require.Equal(t, "010-1234-5678", user.CellPhone)
	require.Equal(t, "EMP001", user.EmployeeNumber)
	require.Equal(t, []WorksmobileUserOrganization{
		{
			Primary: true,
			OrgUnits: []WorksmobileUserOrgUnit{
				{OrgUnitID: "externalKey:primary-org", Primary: true, IsManager: boolPtr(false)},
				{OrgUnitID: "externalKey:secondary-org", Primary: false, IsManager: boolPtr(true)},
			},
		},
	}, user.Organizations)
}

func TestParseWorksmobileDirectoryGroupExtractsMailLocalPart(t *testing.T) {
	group := parseWorksmobileDirectoryGroup(map[string]any{
		"orgUnitId":   "works-org-1",
		"orgUnitName": "기술개발센터",
		"email":       "tech-dev-center@samaneng.com",
	})

	require.Equal(t, "tech-dev-center@samaneng.com", group.Email)
	require.Equal(t, "tech-dev-center", group.MailLocalPart)
}

func boolPtr(value bool) *bool {
	return &value
}

type fakeWorksmobileOutboxRepo struct {
	recent                     []domain.WorksmobileOutbox
	ready                      []domain.WorksmobileOutbox
	created                    []domain.WorksmobileOutbox
	credentialBatchJobs        []domain.WorksmobileOutbox
	payloadUpdates             []domain.JSONMap
	deletedPendingTenantRootID string
	deletedPendingCount        int
	listReadyCalls             int
	listReadyPanic             any
	markProcessingClaims       map[string]bool
	processingIDs              []string
	processedIDs               []string
	failedIDs                  []string
}

func (f *fakeWorksmobileOutboxRepo) Create(ctx context.Context, item *domain.WorksmobileOutbox) error {
	f.created = append(f.created, *item)
	return nil
}

func (f *fakeWorksmobileOutboxRepo) ListRecent(ctx context.Context, limit int) ([]domain.WorksmobileOutbox, error) {
	return f.recent, nil
}

func (f *fakeWorksmobileOutboxRepo) ListCredentialBatchJobs(ctx context.Context, tenantRootID, credentialBatchID string) ([]domain.WorksmobileOutbox, error) {
	rows := make([]domain.WorksmobileOutbox, 0)
	for _, row := range f.credentialBatchJobs {
		if stringValue(row.Payload["tenantRootId"]) != tenantRootID {
			continue
		}
		if credentialBatchID != "" && stringValue(row.Payload["credentialBatchId"]) != credentialBatchID {
			continue
		}
		rows = append(rows, row)
	}
	return rows, nil
}

func (f *fakeWorksmobileOutboxRepo) UpdatePayload(ctx context.Context, id string, payload domain.JSONMap) error {
	f.payloadUpdates = append(f.payloadUpdates, payload)
	for i := range f.credentialBatchJobs {
		if f.credentialBatchJobs[i].ID == id {
			f.credentialBatchJobs[i].Payload = payload
		}
	}
	return nil
}

func (f *fakeWorksmobileOutboxRepo) DeletePendingByTenantRoot(ctx context.Context, tenantRootID string) (int64, error) {
	f.deletedPendingTenantRootID = tenantRootID
	return int64(f.deletedPendingCount), nil
}

func (f *fakeWorksmobileOutboxRepo) ListReady(ctx context.Context, limit int) ([]domain.WorksmobileOutbox, error) {
	f.listReadyCalls++
	if f.listReadyPanic != nil {
		panic(f.listReadyPanic)
	}
	return f.ready, nil
}

func (f *fakeWorksmobileOutboxRepo) FindByID(ctx context.Context, id string) (*domain.WorksmobileOutbox, error) {
	return nil, nil
}

func (f *fakeWorksmobileOutboxRepo) MarkRetry(ctx context.Context, id string) error {
	return nil
}

func (f *fakeWorksmobileOutboxRepo) MarkProcessing(ctx context.Context, id string) (bool, error) {
	if f.markProcessingClaims != nil && !f.markProcessingClaims[id] {
		return false, nil
	}
	f.processingIDs = append(f.processingIDs, id)
	return true, nil
}

func (f *fakeWorksmobileOutboxRepo) MarkProcessed(ctx context.Context, id string) error {
	f.processedIDs = append(f.processedIDs, id)
	return nil
}

func (f *fakeWorksmobileOutboxRepo) MarkFailed(ctx context.Context, id string, message string, nextAttemptAt time.Time) error {
	f.failedIDs = append(f.failedIDs, id)
	return nil
}

type fakeWorksmobileDirectoryClient struct {
	createdOrgUnits  []WorksmobileOrgUnitPayload
	deletedOrgUnits  []string
	createdUsers     []WorksmobileUserPayload
	deletedUsers     []string
	activeUsers      []string
	suspendedUsers   []string
	aliasEmails      []string
	passwordResets   []string
	users            []WorksmobileRemoteUser
	orgUnitMatchKeys []string
	groups           []WorksmobileRemoteGroup
}

type captureRoundTripper struct {
	request       *http.Request
	requestBody   []byte
	statusCode    int
	body          string
	responses     []captureResponse
	requests      []*http.Request
	requestBodies [][]byte
}

type captureResponse struct {
	statusCode int
	body       string
}

type captureWorksmobileRateLimiter struct {
	keys []string
}

func (l *captureWorksmobileRateLimiter) Wait(ctx context.Context, key string) error {
	l.keys = append(l.keys, key)
	return ctx.Err()
}

type fakeWorksmobileRelayLeaderLock struct {
	held        bool
	ensureCalls int
}

func (l *fakeWorksmobileRelayLeaderLock) EnsureLeadership(ctx context.Context) (bool, error) {
	l.ensureCalls++
	return l.held, ctx.Err()
}

func (t *captureRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	t.request = req
	t.requests = append(t.requests, req)
	if req.Body != nil {
		data, err := io.ReadAll(req.Body)
		if err != nil {
			return nil, err
		}
		t.requestBody = data
		t.requestBodies = append(t.requestBodies, data)
	}
	statusCode := t.statusCode
	body := t.body
	if len(t.responses) > 0 {
		response := t.responses[0]
		t.responses = t.responses[1:]
		statusCode = response.statusCode
		body = response.body
	}
	if statusCode == 0 {
		statusCode = http.StatusOK
	}
	return &http.Response{
		StatusCode: statusCode,
		Header:     make(http.Header),
		Body:       io.NopCloser(strings.NewReader(body)),
		Request:    req,
	}, nil
}

func testRSAPrivateKeyPEM(t *testing.T) string {
	t.Helper()
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	require.NoError(t, err)
	data := x509.MarshalPKCS1PrivateKey(key)
	return string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: data}))
}

func getenvDefault(key string, fallback string) string {
	if value := os.Getenv(key); value != "" {
		return value
	}
	return fallback
}

func newWorksmobileLiveClient() *WorksmobileHTTPClient {
	client := NewWorksmobileHTTPClientWithAuth("", os.Getenv("SAMAN_SCIM_LONGLIVE_TOKEN"), WorksmobileOAuthConfig{
		ClientID:       os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_ID"),
		ClientSecret:   os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SECRET"),
		ServiceAccount: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT"),
		PrivateKey:     os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY"),
		Scope:          getenvDefault("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
		TokenURL:       os.Getenv("WORKS_ADMIN_OAUTH_TOKEN_URL"),
	})
	client.BaseURL = os.Getenv("WORKS_ADMIN_API_BASE_URL")
	return client
}

func (f *fakeWorksmobileDirectoryClient) CreateOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload) error {
	f.createdOrgUnits = append(f.createdOrgUnits, payload)
	return nil
}

func (f *fakeWorksmobileDirectoryClient) UpsertOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload, matchLocalPart string) error {
	f.createdOrgUnits = append(f.createdOrgUnits, payload)
	f.orgUnitMatchKeys = append(f.orgUnitMatchKeys, matchLocalPart)
	return nil
}

func (f *fakeWorksmobileDirectoryClient) DeleteOrgUnit(ctx context.Context, orgUnitID string) error {
	f.deletedOrgUnits = append(f.deletedOrgUnits, orgUnitID)
	return nil
}

func (f *fakeWorksmobileDirectoryClient) CreateUser(ctx context.Context, payload WorksmobileUserPayload) error {
	f.createdUsers = append(f.createdUsers, payload)
	return nil
}

func (f *fakeWorksmobileDirectoryClient) UpsertUser(ctx context.Context, payload WorksmobileUserPayload) error {
	f.createdUsers = append(f.createdUsers, payload)
	return nil
}

func (f *fakeWorksmobileDirectoryClient) AddUserAliasEmail(ctx context.Context, userID string, email string) error {
	f.aliasEmails = append(f.aliasEmails, userID+":"+email)
	return nil
}

func (f *fakeWorksmobileDirectoryClient) ResetUserPassword(ctx context.Context, userID string, password string) error {
	f.passwordResets = append(f.passwordResets, userID+":"+password)
	return nil
}

func (f *fakeWorksmobileDirectoryClient) DeleteUser(ctx context.Context, userID string) error {
	f.deletedUsers = append(f.deletedUsers, userID)
	return nil
}

func (f *fakeWorksmobileDirectoryClient) SetUserActive(ctx context.Context, userID string, active bool) error {
	if active {
		f.activeUsers = append(f.activeUsers, userID)
	} else {
		f.suspendedUsers = append(f.suspendedUsers, userID)
	}
	return nil
}

func (f *fakeWorksmobileDirectoryClient) ListUsers(ctx context.Context) ([]WorksmobileRemoteUser, error) {
	return f.users, nil
}

func (f *fakeWorksmobileDirectoryClient) ListGroups(ctx context.Context) ([]WorksmobileRemoteGroup, error) {
	return f.groups, nil
}
