package host_test

import (
	"context"
	"fmt"
	"testing"
	"time"

	"hyapp/pkg/xerr"
	hostdomain "hyapp/services/user-service/internal/domain/host"
	userdomain "hyapp/services/user-service/internal/domain/user"
	hostservice "hyapp/services/user-service/internal/service/host"
	"hyapp/services/user-service/internal/testutil/mysqltest"
)

type sequenceIDGenerator struct {
	next int64
}

func (g *sequenceIDGenerator) NewInt64() int64 {
	id := g.next
	g.next++
	return id
}

func newHostService(repository *mysqltest.Repository, firstID int64, nowMs int64) *hostservice.Service {
	return hostservice.New(repository.HostRepository(),
		hostservice.WithIDGenerator(&sequenceIDGenerator{next: firstID}),
		hostservice.WithClock(func() time.Time { return time.UnixMilli(nowMs) }),
	)
}

func seedActiveUser(t *testing.T, repository *mysqltest.Repository, userID int64, regionID int64) {
	t.Helper()
	seedActiveUserInCountry(t, repository, userID, regionID, "CN", "")
}

func seedActiveUserInCountry(t *testing.T, repository *mysqltest.Repository, userID int64, regionID int64, country string, avatar string) {
	t.Helper()
	repository.PutUser(userdomain.User{
		UserID:               userID,
		DefaultDisplayUserID: displayID(userID),
		CurrentDisplayUserID: displayID(userID),
		Country:              country,
		RegionID:             regionID,
		Avatar:               avatar,
		ProfileCompleted:     true,
		ProfileCompletedAtMs: 1,
		OnboardingStatus:     userdomain.OnboardingStatusCompleted,
		Status:               userdomain.StatusActive,
	})
}

func displayID(userID int64) string {
	return fmt.Sprintf("%06d", 900000+userID%100000)
}

func TestCheckBusinessCapabilityRequiresActiveAgencyOwner(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedActiveUser(t, repository, 101, 10)
	seedActiveUser(t, repository, 202, 10)
	repository.PutAgency(hostdomain.Agency{
		AgencyID:    301,
		OwnerUserID: 101,
		RegionID:    10,
		Name:        "Manager Agency",
		Status:      hostdomain.AgencyStatusActive,
	})
	svc := newHostService(repository, 1000, 2000)

	allowed, reason, err := svc.CheckBusinessCapability(ctx, 101, hostservice.CapabilityManagerResourceGrant)
	if err != nil {
		t.Fatalf("CheckBusinessCapability for owner failed: %v", err)
	}
	if !allowed || reason != "" {
		t.Fatalf("active agency owner should be allowed: allowed=%v reason=%q", allowed, reason)
	}

	allowed, reason, err = svc.CheckBusinessCapability(ctx, 202, hostservice.CapabilityManagerResourceGrant)
	if err != nil {
		t.Fatalf("CheckBusinessCapability for non-owner failed: %v", err)
	}
	if allowed || reason != "active_agency_owner_required" {
		t.Fatalf("non-owner should be denied by stable reason: allowed=%v reason=%q", allowed, reason)
	}

	allowed, _, err = svc.CheckBusinessCapability(ctx, 101, "unknown_capability")
	if !xerr.IsCode(err, xerr.InvalidArgument) || allowed {
		t.Fatalf("unknown capability must fail closed: allowed=%v err=%v", allowed, err)
	}
}

func TestApplyReviewApproveCreatesHostMembershipIdempotently(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedActiveUser(t, repository, 101, 10)
	seedActiveUser(t, repository, 202, 10)
	repository.PutAgency(hostdomain.Agency{
		AgencyID:       301,
		OwnerUserID:    101,
		RegionID:       10,
		ParentBDUserID: 501,
		Name:           "Phase One Agency",
		Status:         hostdomain.AgencyStatusActive,
		JoinEnabled:    true,
		CreatedAtMs:    100,
		UpdatedAtMs:    100,
	})
	svc := newHostService(repository, 1000, 2000)

	application, err := svc.ApplyToAgency(ctx, hostservice.ApplyToAgencyInput{
		CommandID: "apply-202-301",
		UserID:    202,
		AgencyID:  301,
	})
	if err != nil {
		t.Fatalf("ApplyToAgency failed: %v", err)
	}
	if application.Status != hostdomain.ApplicationStatusPending || application.RegionID != 10 {
		t.Fatalf("application mismatch: %+v", application)
	}

	retriedApplication, err := svc.ApplyToAgency(ctx, hostservice.ApplyToAgencyInput{
		CommandID: "apply-202-301",
		UserID:    202,
		AgencyID:  301,
	})
	if err != nil {
		t.Fatalf("retry ApplyToAgency failed: %v", err)
	}
	if retriedApplication.ApplicationID != application.ApplicationID {
		t.Fatalf("apply retry must return same application: got %d want %d", retriedApplication.ApplicationID, application.ApplicationID)
	}

	result, err := svc.ReviewAgencyApplication(ctx, hostservice.ReviewAgencyApplicationInput{
		CommandID:      "review-approve-202-301",
		ReviewerUserID: 101,
		ApplicationID:  application.ApplicationID,
		Decision:       hostdomain.ApplicationDecisionApprove,
	})
	if err != nil {
		t.Fatalf("ReviewAgencyApplication approve failed: %v", err)
	}
	if result.Application.Status != hostdomain.ApplicationStatusApproved {
		t.Fatalf("approved application mismatch: %+v", result.Application)
	}
	if result.HostProfile.UserID != 202 || result.HostProfile.CurrentAgencyID != 301 || result.Membership.Status != hostdomain.MembershipStatusActive {
		t.Fatalf("host membership facts mismatch: profile=%+v membership=%+v", result.HostProfile, result.Membership)
	}

	retriedReview, err := svc.ReviewAgencyApplication(ctx, hostservice.ReviewAgencyApplicationInput{
		CommandID:      "review-approve-202-301",
		ReviewerUserID: 101,
		ApplicationID:  application.ApplicationID,
		Decision:       hostdomain.ApplicationDecisionApprove,
	})
	if err != nil {
		t.Fatalf("retry ReviewAgencyApplication failed: %v", err)
	}
	if retriedReview.Membership.MembershipID != result.Membership.MembershipID {
		t.Fatalf("review retry must return same membership: got %d want %d", retriedReview.Membership.MembershipID, result.Membership.MembershipID)
	}

	_, err = svc.ApplyToAgency(ctx, hostservice.ApplyToAgencyInput{
		CommandID: "apply-second-active-member",
		UserID:    202,
		AgencyID:  301,
	})
	if got := xerr.CodeOf(err); got != xerr.Conflict {
		t.Fatalf("active member must not create another application: got %s err=%v", got, err)
	}
}

func TestAcceptAgencyInvitationCreatesOwnerHostAndReusesDetachedHost(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedActiveUser(t, repository, 501, 10)
	seedActiveUser(t, repository, 601, 10)
	seedActiveUser(t, repository, 602, 10)
	repository.PutBDProfile(hostdomain.BDProfile{
		UserID:   501,
		Role:     hostdomain.BDRoleBD,
		RegionID: 10,
		Status:   hostdomain.BDStatusActive,
	})
	svc := newHostService(repository, 2000, 3000)

	invitation, err := svc.InviteAgency(ctx, hostservice.InviteAgencyInput{
		CommandID:     "invite-agency-601",
		InviterUserID: 501,
		TargetUserID:  601,
		AgencyName:    "Owner Agency",
	})
	if err != nil {
		t.Fatalf("InviteAgency failed: %v", err)
	}
	if invitation.Status != hostdomain.InvitationStatusAccepted {
		t.Fatalf("agency invitation should be accepted immediately: %+v", invitation)
	}
	summary, err := svc.GetUserRoleSummary(ctx, 601)
	if err != nil {
		t.Fatalf("GetUserRoleSummary failed: %v", err)
	}
	agency, err := svc.GetAgency(ctx, summary.AgencyID)
	if err != nil {
		t.Fatalf("GetAgency failed: %v", err)
	}
	hostProfile, err := svc.GetHostProfile(ctx, 601)
	if err != nil {
		t.Fatalf("GetHostProfile failed: %v", err)
	}
	if !summary.IsAgency || !summary.IsHost || agency.OwnerUserID != 601 || hostProfile.CurrentAgencyID != agency.AgencyID {
		t.Fatalf("owner agency facts mismatch: summary=%+v agency=%+v host=%+v", summary, agency, hostProfile)
	}
	repository.PutHostProfile(hostdomain.HostProfile{
		UserID:   602,
		Status:   hostdomain.HostStatusActive,
		RegionID: 10,
		Source:   "test",
	})
	secondInvitation, err := svc.InviteAgency(ctx, hostservice.InviteAgencyInput{
		CommandID:     "invite-agency-602",
		InviterUserID: 501,
		TargetUserID:  602,
		AgencyName:    "Reused Host Agency",
	})
	if err != nil {
		t.Fatalf("detached active host should become agency owner: %v", err)
	}
	secondSummary, err := svc.GetUserRoleSummary(ctx, 602)
	if err != nil {
		t.Fatalf("GetUserRoleSummary for reused host failed: %v", err)
	}
	secondHostProfile, err := svc.GetHostProfile(ctx, 602)
	if err != nil {
		t.Fatalf("GetHostProfile for reused host failed: %v", err)
	}
	if !secondSummary.IsAgency || !secondSummary.IsHost || secondHostProfile.CurrentAgencyID <= 0 || secondHostProfile.Source != "test" || secondInvitation.Status != hostdomain.InvitationStatusAccepted {
		t.Fatalf("detached host reuse mismatch: summary=%+v host=%+v invitation=%+v", secondSummary, secondHostProfile, secondInvitation)
	}
	_, err = svc.InviteAgency(ctx, hostservice.InviteAgencyInput{
		CommandID:     "invite-agency-602-again",
		InviterUserID: 501,
		TargetUserID:  602,
		AgencyName:    "Blocked Agency",
	})
	if got := xerr.CodeOf(err); got != xerr.Conflict {
		t.Fatalf("host with active membership must not become another agency owner: got %s err=%v", got, err)
	}
}

func TestAcceptBDInvitationDoesNotCreateHostProfile(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedActiveUser(t, repository, 701, 10)
	seedActiveUser(t, repository, 702, 10)
	repository.PutBDProfile(hostdomain.BDProfile{
		UserID:   701,
		Role:     hostdomain.BDRoleLeader,
		RegionID: 10,
		Status:   hostdomain.BDStatusActive,
	})
	svc := newHostService(repository, 3000, 4000)

	invitation, err := svc.InviteBD(ctx, hostservice.InviteBDInput{
		CommandID:     "invite-bd-702",
		InviterUserID: 701,
		TargetUserID:  702,
	})
	if err != nil {
		t.Fatalf("InviteBD failed: %v", err)
	}
	if invitation.Status != hostdomain.InvitationStatusAccepted {
		t.Fatalf("bd invitation should be accepted immediately: %+v", invitation)
	}
	bdProfile, err := svc.GetBDProfile(ctx, 702, hostdomain.BDRoleBD)
	if err != nil {
		t.Fatalf("GetBDProfile failed: %v", err)
	}
	if bdProfile.UserID != 702 || bdProfile.Role != hostdomain.BDRoleBD || bdProfile.ParentLeaderUserID != 701 {
		t.Fatalf("bd profile mismatch: %+v", bdProfile)
	}
	if _, err := svc.GetHostProfile(ctx, 702); xerr.CodeOf(err) != xerr.NotFound {
		t.Fatalf("becoming bd must not create host profile: err=%v", err)
	}
}

func TestInviteBDAcceptsSelfInviteForLeader(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedActiveUser(t, repository, 703, 10)
	repository.PutBDProfile(hostdomain.BDProfile{
		UserID:   703,
		Role:     hostdomain.BDRoleLeader,
		RegionID: 10,
		Status:   hostdomain.BDStatusActive,
	})
	svc := newHostService(repository, 3100, 4100)

	invitation, err := svc.InviteBD(ctx, hostservice.InviteBDInput{
		CommandID:     "invite-bd-self-703",
		InviterUserID: 703,
		TargetUserID:  703,
	})
	if err != nil {
		t.Fatalf("bd leader self invite must be accepted: %v", err)
	}
	if invitation.Status != hostdomain.InvitationStatusAccepted || invitation.InviterUserID != 703 || invitation.TargetUserID != 703 || invitation.ParentLeaderUserID != 703 {
		t.Fatalf("self invite invitation mismatch: %+v", invitation)
	}
	ordinaryBD, err := svc.GetBDProfile(ctx, 703, hostdomain.BDRoleBD)
	if err != nil {
		t.Fatalf("self invite must create ordinary bd profile: %v", err)
	}
	if ordinaryBD.Role != hostdomain.BDRoleBD || ordinaryBD.ParentLeaderUserID != 703 {
		t.Fatalf("self invite ordinary bd mismatch: %+v", ordinaryBD)
	}
	leaderProfile, err := svc.GetBDProfile(ctx, 703, hostdomain.BDRoleLeader)
	if err != nil {
		t.Fatalf("self invite must keep existing leader profile unchanged: %v", err)
	}
	if leaderProfile.Role != hostdomain.BDRoleLeader || leaderProfile.Status != hostdomain.BDStatusActive {
		t.Fatalf("self invite leader profile mismatch: %+v", leaderProfile)
	}
	retried, err := svc.InviteBD(ctx, hostservice.InviteBDInput{
		CommandID:     "invite-bd-self-703",
		InviterUserID: 703,
		TargetUserID:  703,
	})
	if err != nil || retried.InvitationID != invitation.InvitationID {
		t.Fatalf("self invite retry must replay existing invitation: retried=%+v err=%v", retried, err)
	}
}

func TestAdminCreateBDLeaderAndBDIdempotently(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	repository.PutRegion(userdomain.Region{RegionID: 20, RegionCode: "R20", Name: "Region 20"})
	seedActiveUser(t, repository, 801, 20)
	seedActiveUser(t, repository, 802, 20)
	seedActiveUser(t, repository, 804, 20)
	svc := newHostService(repository, 4000, 5000)

	leader, err := svc.CreateBDLeader(ctx, hostservice.CreateBDLeaderInput{
		CommandID:    "admin-create-leader-801",
		AdminUserID:  1,
		TargetUserID: 801,
		Reason:       "seed leader",
		RequestID:    "req-leader-1",
	})
	if err != nil {
		t.Fatalf("CreateBDLeader failed: %v", err)
	}
	if leader.Role != hostdomain.BDRoleLeader || leader.Status != hostdomain.BDStatusActive || leader.RegionID != 20 {
		t.Fatalf("leader mismatch: %+v", leader)
	}
	updatedUser, err := repository.GetUser(ctx, 801)
	if err != nil {
		t.Fatalf("GetUser after CreateBDLeader failed: %v", err)
	}
	if updatedUser.RegionID != 20 {
		t.Fatalf("CreateBDLeader must keep target user region as profile region: %+v", updatedUser)
	}
	retriedLeader, err := svc.CreateBDLeader(ctx, hostservice.CreateBDLeaderInput{
		CommandID:    "admin-create-leader-801",
		AdminUserID:  1,
		TargetUserID: 801,
	})
	if err != nil {
		t.Fatalf("retry CreateBDLeader failed: %v", err)
	}
	if retriedLeader.UserID != leader.UserID {
		t.Fatalf("leader retry must replay existing result: %+v", retriedLeader)
	}

	sameUserBD, err := svc.CreateBD(ctx, hostservice.CreateBDInput{
		CommandID:          "admin-create-bd-801-same-user",
		AdminUserID:        1,
		TargetUserID:       801,
		ParentLeaderUserID: 801,
		Reason:             "same user can also be bd",
		RequestID:          "req-bd-same-user-1",
	})
	if err != nil {
		t.Fatalf("CreateBD for existing leader user failed: %v", err)
	}
	if sameUserBD.Role != hostdomain.BDRoleBD || sameUserBD.ParentLeaderUserID != 801 {
		t.Fatalf("same user bd mismatch: %+v", sameUserBD)
	}
	leaderAfterBD, err := svc.GetBDProfile(ctx, 801, hostdomain.BDRoleLeader)
	if err != nil || leaderAfterBD.Role != hostdomain.BDRoleLeader || leaderAfterBD.Status != hostdomain.BDStatusActive {
		t.Fatalf("ordinary bd creation must not rewrite leader row: leader=%+v err=%v", leaderAfterBD, err)
	}

	bd, err := svc.CreateBD(ctx, hostservice.CreateBDInput{
		CommandID:          "admin-create-bd-802",
		AdminUserID:        1,
		TargetUserID:       802,
		ParentLeaderUserID: 801,
		Reason:             "seed bd",
		RequestID:          "req-bd-1",
	})
	if err != nil {
		t.Fatalf("CreateBD failed: %v", err)
	}
	if bd.Role != hostdomain.BDRoleBD || bd.ParentLeaderUserID != 801 || bd.RegionID != 20 {
		t.Fatalf("bd mismatch: %+v", bd)
	}

	independentBD, err := svc.CreateBD(ctx, hostservice.CreateBDInput{
		CommandID:    "admin-create-bd-804-independent",
		AdminUserID:  1,
		TargetUserID: 804,
		Reason:       "seed independent bd",
		RequestID:    "req-bd-independent-1",
	})
	if err != nil {
		t.Fatalf("CreateBD without parent leader failed: %v", err)
	}
	if independentBD.Role != hostdomain.BDRoleBD || independentBD.ParentLeaderUserID != 0 || independentBD.RegionID != 20 {
		t.Fatalf("independent bd mismatch: %+v", independentBD)
	}

	disabled, err := svc.SetBDStatus(ctx, hostservice.SetBDStatusInput{
		CommandID:    "admin-disable-bd-802",
		AdminUserID:  1,
		TargetUserID: 802,
		Role:         hostdomain.BDRoleBD,
		Status:       hostdomain.BDStatusDisabled,
		Reason:       "stop bd",
		RequestID:    "req-bd-stop-1",
	})
	if err != nil {
		t.Fatalf("SetBDStatus disabled failed: %v", err)
	}
	if disabled.Status != hostdomain.BDStatusDisabled {
		t.Fatalf("disabled bd mismatch: %+v", disabled)
	}
	_, err = svc.InviteBD(ctx, hostservice.InviteBDInput{
		CommandID:     "invite-disabled-bd-802-again",
		InviterUserID: 801,
		TargetUserID:  802,
	})
	if got := xerr.CodeOf(err); got != xerr.Conflict {
		t.Fatalf("disabled ordinary bd must not be invited again: got %s err=%v", got, err)
	}
	disabledSameUserBD, err := svc.SetBDStatus(ctx, hostservice.SetBDStatusInput{
		CommandID:    "admin-disable-bd-801",
		AdminUserID:  1,
		TargetUserID: 801,
		Role:         hostdomain.BDRoleBD,
		Status:       hostdomain.BDStatusDisabled,
		Reason:       "stop ordinary bd only",
		RequestID:    "req-bd-stop-801",
	})
	if err != nil {
		t.Fatalf("SetBDStatus ordinary bd for same user failed: %v", err)
	}
	if disabledSameUserBD.Role != hostdomain.BDRoleBD || disabledSameUserBD.Status != hostdomain.BDStatusDisabled {
		t.Fatalf("disabled same user bd mismatch: %+v", disabledSameUserBD)
	}
	stillLeader, err := svc.GetBDProfile(ctx, 801, hostdomain.BDRoleLeader)
	if err != nil || stillLeader.Status != hostdomain.BDStatusActive {
		t.Fatalf("disabling ordinary bd must not affect leader: leader=%+v err=%v", stillLeader, err)
	}
	disabledLeader, err := svc.SetBDStatus(ctx, hostservice.SetBDStatusInput{
		CommandID:    "admin-disable-leader-801",
		AdminUserID:  1,
		TargetUserID: 801,
		Role:         hostdomain.BDRoleLeader,
		Status:       hostdomain.BDStatusDisabled,
		Reason:       "stop leader only",
		RequestID:    "req-leader-stop-801",
	})
	if err != nil {
		t.Fatalf("SetBDStatus leader failed: %v", err)
	}
	if disabledLeader.Role != hostdomain.BDRoleLeader || disabledLeader.Status != hostdomain.BDStatusDisabled {
		t.Fatalf("disabled leader mismatch: %+v", disabledLeader)
	}
	ordinaryStillDisabled, err := svc.GetBDProfile(ctx, 801, hostdomain.BDRoleBD)
	if err != nil || ordinaryStillDisabled.Status != hostdomain.BDStatusDisabled {
		t.Fatalf("disabling leader must not rewrite ordinary bd: bd=%+v err=%v", ordinaryStillDisabled, err)
	}
}

func TestAdminCreateBDLeaderRejectsDisabledRegion(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	repository.PutRegion(userdomain.Region{RegionID: 21, RegionCode: "R21", Name: "Region 21", Status: userdomain.RegionStatusDisabled})
	seedActiveUser(t, repository, 803, 21)
	svc := newHostService(repository, 4100, 5100)

	_, err := svc.CreateBDLeader(ctx, hostservice.CreateBDLeaderInput{
		CommandID:    "admin-create-leader-disabled-region",
		AdminUserID:  1,
		TargetUserID: 803,
	})
	if got := xerr.CodeOf(err); got != xerr.RegionDisabled {
		t.Fatalf("disabled region must be rejected: got %s err=%v", got, err)
	}
}

func TestAdminCreateAgencyControlsAppSearch(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedActiveUserInCountry(t, repository, 901, 30, "CN", "https://cdn.example/owner-901.png")
	seedActiveUser(t, repository, 902, 30)
	seedActiveUserInCountry(t, repository, 903, 30, "US", "")
	repository.PutBDProfile(hostdomain.BDProfile{
		UserID:   900,
		Role:     hostdomain.BDRoleLeader,
		RegionID: 30,
		Status:   hostdomain.BDStatusActive,
	})
	svc := newHostService(repository, 5000, 6000)

	created, err := svc.CreateAgency(ctx, hostservice.CreateAgencyInput{
		CommandID:      "admin-create-agency-901",
		AdminUserID:    1,
		OwnerUserID:    901,
		ParentBDUserID: 900,
		Name:           "Admin Seed Agency",
		JoinEnabled:    true,
		Reason:         "seed agency",
		RequestID:      "req-agency-1",
	})
	if err != nil {
		t.Fatalf("CreateAgency failed: %v", err)
	}
	if created.Agency.OwnerUserID != 901 || created.Agency.ParentBDUserID != 900 || !created.Agency.JoinEnabled {
		t.Fatalf("agency mismatch: %+v", created.Agency)
	}
	if created.HostProfile.CurrentAgencyID != created.Agency.AgencyID || created.HostProfile.Source != hostdomain.HostSourceAdminCreateAgency {
		t.Fatalf("owner host profile mismatch: %+v", created.HostProfile)
	}
	if created.Membership.MembershipType != hostdomain.MembershipTypeOwner || created.Membership.Status != hostdomain.MembershipStatusActive {
		t.Fatalf("owner membership mismatch: %+v", created.Membership)
	}
	if _, err := svc.CreateAgency(ctx, hostservice.CreateAgencyInput{
		CommandID:      "admin-create-agency-903",
		AdminUserID:    1,
		OwnerUserID:    903,
		ParentBDUserID: 900,
		Name:           "Other Country Agency",
		JoinEnabled:    true,
		Reason:         "seed agency",
		RequestID:      "req-agency-2",
	}); err != nil {
		t.Fatalf("CreateAgency in other country failed: %v", err)
	}
	agencies, err := svc.SearchAgencies(ctx, 902, "", 20)
	if err != nil {
		t.Fatalf("SearchAgencies list failed: %v", err)
	}
	if len(agencies) != 1 || agencies[0].AgencyID != created.Agency.AgencyID || agencies[0].OwnerAvatar != "https://cdn.example/owner-901.png" || agencies[0].ActiveHostCount != 1 {
		t.Fatalf("created agency list should be scoped by country with display fields: %+v", agencies)
	}
	agencies, err = svc.SearchAgencies(ctx, 902, "Admin Seed", 20)
	if err != nil {
		t.Fatalf("SearchAgencies failed: %v", err)
	}
	if len(agencies) != 1 || agencies[0].AgencyID != created.Agency.AgencyID {
		t.Fatalf("created agency should be searchable: %+v", agencies)
	}
	agencies, err = svc.SearchAgencies(ctx, 902, displayID(901), 20)
	if err != nil {
		t.Fatalf("SearchAgencies by owner display id failed: %v", err)
	}
	if len(agencies) != 1 || agencies[0].AgencyID != created.Agency.AgencyID {
		t.Fatalf("created agency should be searchable by owner display id: %+v", agencies)
	}

	disabledJoin, err := svc.SetAgencyJoinEnabled(ctx, hostservice.SetAgencyJoinEnabledInput{
		CommandID:   "admin-disable-agency-join-901",
		AdminUserID: 1,
		AgencyID:    created.Agency.AgencyID,
		JoinEnabled: false,
		Reason:      "pause join",
		RequestID:   "req-agency-join-1",
	})
	if err != nil {
		t.Fatalf("SetAgencyJoinEnabled failed: %v", err)
	}
	if disabledJoin.JoinEnabled {
		t.Fatalf("agency join should be disabled: %+v", disabledJoin)
	}
	agencies, err = svc.SearchAgencies(ctx, 902, "Admin Seed", 20)
	if err != nil {
		t.Fatalf("SearchAgencies after join disabled failed: %v", err)
	}
	if len(agencies) != 0 {
		t.Fatalf("join-disabled agency must not be searchable: %+v", agencies)
	}

	closed, err := svc.CloseAgency(ctx, hostservice.CloseAgencyInput{
		CommandID:   "admin-close-agency-901",
		AdminUserID: 1,
		AgencyID:    created.Agency.AgencyID,
		Reason:      "close agency",
		RequestID:   "req-agency-close-1",
	})
	if err != nil {
		t.Fatalf("CloseAgency failed: %v", err)
	}
	if closed.Status != hostdomain.AgencyStatusClosed || closed.JoinEnabled {
		t.Fatalf("closed agency mismatch: %+v", closed)
	}

	deleted, err := svc.DeleteAgency(ctx, hostservice.DeleteAgencyInput{
		CommandID:   "admin-delete-agency-901",
		AdminUserID: 1,
		AgencyID:    created.Agency.AgencyID,
		Reason:      "delete agency",
		RequestID:   "req-agency-delete-1",
	})
	if err != nil {
		t.Fatalf("DeleteAgency failed: %v", err)
	}
	if deleted.Status != hostdomain.AgencyStatusDeleted || deleted.JoinEnabled {
		t.Fatalf("deleted agency mismatch: %+v", deleted)
	}
	hostProfile, err := svc.GetHostProfile(ctx, 901)
	if err != nil {
		t.Fatalf("GetHostProfile after delete failed: %v", err)
	}
	if hostProfile.CurrentAgencyID != 0 || hostProfile.CurrentMembershipID != 0 || hostProfile.Status != hostdomain.HostStatusActive {
		t.Fatalf("delete agency should detach but keep host profile: %+v", hostProfile)
	}
	recreated, err := svc.CreateAgency(ctx, hostservice.CreateAgencyInput{
		CommandID:      "admin-create-agency-901-after-delete",
		AdminUserID:    1,
		OwnerUserID:    901,
		ParentBDUserID: 900,
		Name:           "Recreated Agency",
		JoinEnabled:    true,
		Reason:         "recreate agency",
		RequestID:      "req-agency-recreate-1",
	})
	if err != nil {
		t.Fatalf("detached owner host should be reusable after delete: %v", err)
	}
	if recreated.HostProfile.CurrentAgencyID != recreated.Agency.AgencyID || recreated.HostProfile.FirstBecameHostAtMs != hostProfile.FirstBecameHostAtMs {
		t.Fatalf("recreated agency should reuse host identity: host=%+v agency=%+v oldHost=%+v", recreated.HostProfile, recreated.Agency, hostProfile)
	}
}

func TestAdminCreateAgencyWithoutParentBD(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedActiveUser(t, repository, 904, 31)
	svc := newHostService(repository, 5600, 6600)

	created, err := svc.CreateAgency(ctx, hostservice.CreateAgencyInput{
		CommandID:   "admin-create-agency-904-independent",
		AdminUserID: 1,
		OwnerUserID: 904,
		Name:        "Independent Agency",
		JoinEnabled: true,
		Reason:      "seed independent agency",
		RequestID:   "req-agency-independent-1",
	})
	if err != nil {
		t.Fatalf("CreateAgency without parent bd failed: %v", err)
	}
	if created.Agency.OwnerUserID != 904 || created.Agency.ParentBDUserID != 0 || created.Agency.RegionID != 31 || !created.Agency.JoinEnabled {
		t.Fatalf("independent agency mismatch: %+v", created.Agency)
	}
	if created.HostProfile.CurrentAgencyID != created.Agency.AgencyID || created.Membership.MembershipType != hostdomain.MembershipTypeOwner {
		t.Fatalf("independent agency owner facts mismatch: host=%+v membership=%+v", created.HostProfile, created.Membership)
	}
}

func TestAdminCreateAgencyReusesDetachedHost(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedActiveUser(t, repository, 911, 40)
	repository.PutBDProfile(hostdomain.BDProfile{
		UserID:   910,
		Role:     hostdomain.BDRoleBD,
		RegionID: 40,
		Status:   hostdomain.BDStatusActive,
	})
	repository.PutHostProfile(hostdomain.HostProfile{
		UserID:   911,
		Status:   hostdomain.HostStatusActive,
		RegionID: 40,
		Source:   "test",
	})
	svc := newHostService(repository, 6000, 7000)

	created, err := svc.CreateAgency(ctx, hostservice.CreateAgencyInput{
		CommandID:      "admin-create-agency-existing-host",
		AdminUserID:    1,
		OwnerUserID:    911,
		ParentBDUserID: 910,
		Name:           "Reused Host Agency",
		JoinEnabled:    true,
	})
	if err != nil {
		t.Fatalf("detached host should become agency owner: %v", err)
	}
	if created.HostProfile.CurrentAgencyID != created.Agency.AgencyID || created.HostProfile.Source != "test" {
		t.Fatalf("existing host identity should be reused without rewriting source: %+v", created.HostProfile)
	}
	_, err = svc.CreateAgency(ctx, hostservice.CreateAgencyInput{
		CommandID:      "admin-create-agency-existing-host-again",
		AdminUserID:    1,
		OwnerUserID:    911,
		ParentBDUserID: 910,
		Name:           "Blocked Agency",
		JoinEnabled:    true,
	})
	if got := xerr.CodeOf(err); got != xerr.Conflict {
		t.Fatalf("host with active membership must not become another agency owner: got %s err=%v", got, err)
	}
}

func TestAdminCreateCoinSellerCanCoexistWithBDAndAgency(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedActiveUser(t, repository, 921, 50)
	repository.PutBDProfile(hostdomain.BDProfile{
		UserID:   921,
		Role:     hostdomain.BDRoleLeader,
		RegionID: 50,
		Status:   hostdomain.BDStatusActive,
	})
	repository.PutAgency(hostdomain.Agency{
		AgencyID:       922,
		OwnerUserID:    921,
		RegionID:       50,
		ParentBDUserID: 921,
		Name:           "Seller Agency",
		Status:         hostdomain.AgencyStatusActive,
		JoinEnabled:    true,
		CreatedAtMs:    100,
		UpdatedAtMs:    100,
	})
	svc := newHostService(repository, 7000, 8000)

	profile, err := svc.CreateCoinSeller(ctx, hostservice.CreateCoinSellerInput{
		CommandID:    "admin-create-coin-seller-921",
		AdminUserID:  1,
		TargetUserID: 921,
		Reason:       "seed coin seller",
		RequestID:    "req-coin-seller-1",
	})
	if err != nil {
		t.Fatalf("CreateCoinSeller failed: %v", err)
	}
	if profile.Status != hostdomain.CoinSellerStatusActive || profile.MerchantAssetType != hostdomain.CoinSellerMerchantAssetType {
		t.Fatalf("coin seller profile mismatch: %+v", profile)
	}
	retried, err := svc.CreateCoinSeller(ctx, hostservice.CreateCoinSellerInput{
		CommandID:    "admin-create-coin-seller-921",
		AdminUserID:  1,
		TargetUserID: 921,
	})
	if err != nil {
		t.Fatalf("retry CreateCoinSeller failed: %v", err)
	}
	if retried.UserID != profile.UserID {
		t.Fatalf("coin seller retry must replay existing result: %+v", retried)
	}

	disabled, err := svc.SetCoinSellerStatus(ctx, hostservice.SetCoinSellerStatusInput{
		CommandID:    "admin-disable-coin-seller-921",
		AdminUserID:  1,
		TargetUserID: 921,
		Status:       hostdomain.CoinSellerStatusDisabled,
		Reason:       "pause seller",
	})
	if err != nil {
		t.Fatalf("SetCoinSellerStatus failed: %v", err)
	}
	if disabled.Status != hostdomain.CoinSellerStatusDisabled {
		t.Fatalf("disabled coin seller mismatch: %+v", disabled)
	}
}

func TestListActiveCoinSellersInMyRegionFiltersByCurrentUserRegion(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	repository.PutCountry(userdomain.Country{
		CountryID:          86,
		CountryCode:        "CN",
		CountryName:        "China",
		CountryDisplayName: "中国",
		Enabled:            true,
	})
	repository.PutRegion(userdomain.Region{
		RegionID:   70,
		RegionCode: "east-asia",
		Name:       "East Asia",
		Status:     userdomain.RegionStatusActive,
		Countries:  []string{"CN"},
	})
	repository.PutRegion(userdomain.Region{
		RegionID:   71,
		RegionCode: "west-asia",
		Name:       "West Asia",
		Status:     userdomain.RegionStatusActive,
		Countries:  []string{"CN"},
	})
	seedActiveUser(t, repository, 940, 70)
	repository.PutUser(userdomain.User{
		UserID:               941,
		DefaultDisplayUserID: displayID(941),
		CurrentDisplayUserID: displayID(941),
		Username:             "Seller One",
		Avatar:               "https://cdn.example/seller-one.png",
		Country:              "CN",
		RegionID:             70,
		ProfileCompleted:     true,
		ProfileCompletedAtMs: 1,
		OnboardingStatus:     userdomain.OnboardingStatusCompleted,
		Status:               userdomain.StatusActive,
	})
	repository.PutUser(userdomain.User{
		UserID:               942,
		DefaultDisplayUserID: displayID(942),
		CurrentDisplayUserID: displayID(942),
		Username:             "Disabled Seller",
		Country:              "CN",
		RegionID:             70,
		ProfileCompleted:     true,
		ProfileCompletedAtMs: 1,
		OnboardingStatus:     userdomain.OnboardingStatusCompleted,
		Status:               userdomain.StatusActive,
	})
	repository.PutUser(userdomain.User{
		UserID:               943,
		DefaultDisplayUserID: displayID(943),
		CurrentDisplayUserID: displayID(943),
		Username:             "Other Region Seller",
		Country:              "CN",
		RegionID:             71,
		ProfileCompleted:     true,
		ProfileCompletedAtMs: 1,
		OnboardingStatus:     userdomain.OnboardingStatusCompleted,
		Status:               userdomain.StatusActive,
	})
	svc := newHostService(repository, 9000, 10000)
	for _, userID := range []int64{941, 942, 943} {
		if _, err := svc.CreateCoinSeller(ctx, hostservice.CreateCoinSellerInput{
			CommandID:    fmt.Sprintf("admin-create-coin-seller-%d", userID),
			AdminUserID:  1,
			TargetUserID: userID,
		}); err != nil {
			t.Fatalf("CreateCoinSeller(%d) failed: %v", userID, err)
		}
	}
	if _, err := svc.SetCoinSellerStatus(ctx, hostservice.SetCoinSellerStatusInput{
		CommandID:    "admin-disable-coin-seller-942",
		AdminUserID:  1,
		TargetUserID: 942,
		Status:       hostdomain.CoinSellerStatusDisabled,
	}); err != nil {
		t.Fatalf("SetCoinSellerStatus failed: %v", err)
	}

	items, err := svc.ListActiveCoinSellersInMyRegion(ctx, 940)
	if err != nil {
		t.Fatalf("ListActiveCoinSellersInMyRegion failed: %v", err)
	}
	if len(items) != 1 {
		t.Fatalf("only same-region active sellers should be returned: %+v", items)
	}
	item := items[0]
	if item.UserID != 941 || item.Username != "Seller One" || item.Avatar == "" || item.CountryID != 86 || item.CountryCode != "CN" || item.RegionID != 70 || item.RegionCode != "east-asia" || item.Status != hostdomain.CoinSellerStatusActive {
		t.Fatalf("coin seller item mismatch: %+v", item)
	}
}

func TestGetUserRoleSummaryAggregatesRolesAndPendingInvitations(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedActiveUser(t, repository, 931, 60)
	seedActiveUser(t, repository, 932, 60)
	repository.PutHostProfile(hostdomain.HostProfile{
		UserID:          931,
		Status:          hostdomain.HostStatusActive,
		RegionID:        60,
		CurrentAgencyID: 933,
		Source:          hostdomain.HostSourceAdminCreateAgency,
	})
	repository.PutAgency(hostdomain.Agency{
		AgencyID:       933,
		OwnerUserID:    931,
		RegionID:       60,
		ParentBDUserID: 931,
		Status:         hostdomain.AgencyStatusActive,
		JoinEnabled:    true,
	})
	repository.PutBDProfile(hostdomain.BDProfile{
		UserID:   931,
		Role:     hostdomain.BDRoleLeader,
		RegionID: 60,
		Status:   hostdomain.BDStatusActive,
	})
	svc := newHostService(repository, 8000, 9000)
	if _, err := svc.CreateCoinSeller(ctx, hostservice.CreateCoinSellerInput{
		CommandID:    "admin-create-coin-seller-931",
		AdminUserID:  1,
		TargetUserID: 931,
		Reason:       "summary test",
	}); err != nil {
		t.Fatalf("CreateCoinSeller failed: %v", err)
	}
	if _, err := svc.InviteBD(ctx, hostservice.InviteBDInput{
		CommandID:     "invite-bd-932",
		InviterUserID: 931,
		TargetUserID:  932,
	}); err != nil {
		t.Fatalf("InviteBD failed: %v", err)
	}

	summary, err := svc.GetUserRoleSummary(ctx, 931)
	if err != nil {
		t.Fatalf("GetUserRoleSummary failed: %v", err)
	}
	if !summary.IsHost || !summary.IsAgency || !summary.IsManager || summary.IsBD || !summary.IsBDLeader || !summary.IsCoinSeller || summary.AgencyID != 933 || summary.BDID != 931 {
		t.Fatalf("leader-only role summary mismatch: %+v", summary)
	}
	if _, err := svc.InviteBD(ctx, hostservice.InviteBDInput{
		CommandID:     "invite-bd-self-931",
		InviterUserID: 931,
		TargetUserID:  931,
	}); err != nil {
		t.Fatalf("self InviteBD for summary failed: %v", err)
	}
	combinedSummary, err := svc.GetUserRoleSummary(ctx, 931)
	if err != nil {
		t.Fatalf("GetUserRoleSummary combined failed: %v", err)
	}
	if !combinedSummary.IsBD || !combinedSummary.IsBDLeader || combinedSummary.BDID != 931 {
		t.Fatalf("bd plus leader role summary mismatch: %+v", combinedSummary)
	}
	directBD, err := svc.GetUserRoleSummary(ctx, 932)
	if err != nil {
		t.Fatalf("GetUserRoleSummary direct bd failed: %v", err)
	}
	if directBD.PendingRoleInvitations != 0 || directBD.IsHost || !directBD.IsBD || directBD.BDID != 932 {
		t.Fatalf("direct bd role summary mismatch: %+v", directBD)
	}
}
